Una tarea de sincronización es una función de especificar un rango de tiempo futuro para realizar ciertas tareas. En las aplicaciones web actuales, la mayoría de las aplicaciones tienen funciones de programación de tareas. Tienen su propia sintaxis y soluciones para diferentes voces y sistemas operativos. El sistema operativo Windows lo llama planificación de tareas, y los servicios CRON en Linux proporcionan esta función. Esta función a menudo está involucrada en nuestro sistema de desarrollo comercial. Este chat utilizará el lenguaje Java para completar el uso de tareas de tiempo de uso común en el trabajo de desarrollo diario, con la esperanza de ayudar a todos el trabajo y estudiar.
1. Escenarios de tareas cronometrados
(1) flujo de trabajo de procesamiento de la unidad
Como una nueva orden prepago, se inicializa y se coloca. Si el pedido no se paga dentro del tiempo especificado, se considerará que el pedido de tiempo de espera se cerrará. Se usa ampliamente en el sistema de comercio electrónico. Los usuarios compran productos y generan pedidos, pero no realizan pagos. Si el pedido no se paga dentro de los 30 minutos, el pedido se cerrará (y el número de personas que cumple con este escenario es enorme), y es imposible usar la intervención manual.
(2) Mantenimiento del sistema
El trabajo de envío obtendrá el registro de excepciones del sistema y almacenará algunos datos de punto clave en la base de datos. Se verá a la base de datos a las 11:30 p.m. cada día de la semana (excepto los días festivos y los días de semana), y generará un archivo XML y lo envíe a la dirección de correo electrónico de un determinado empleado.
(3) Proporcionar servicios de recordatorio dentro de la aplicación.
El sistema le recuerda regularmente al usuario iniciado que realice un trabajo relacionado en un momento determinado.
(4) tareas de reconciliación cronometradas
La Compañía y las compañías de tres partes (operadores, bancos, etc.) llevarán a cabo la reconciliación del negocio el mismo día después de la medianoche todos los días, enviarán los datos de resultados de información de conciliación a la dirección de correo electrónico de la persona relevante a cargo y procesarán los datos de desajuste durante las horas de trabajo del día siguiente.
(5) Estadísticas de datos
Hay muchos registros de datos, y la lectura y consulta en tiempo real de la base de datos generarán una cierta cantidad de tiempo, que es para la experiencia del cliente y las necesidades de rendimiento. Por lo tanto, los datos se resumen cada semana (días, horas), de modo que los datos se pueden presentar rápidamente al mostrar los datos.
Hay muchos escenarios en los que se utilizan tareas de tiempo ... parece que las tareas de tiempo se usan realmente ampliamente en nuestro desarrollo diario ...
2. Explicación del temporizador de tecnología de tareas de tiempo convencional
Creo que todos ya están muy familiarizados con java.util.timer. Es la forma más fácil de implementar la programación de tareas. Aquí hay un ejemplo específico:
paquete com.ibm.scheduler; import java.util.timer; import java.util.timerTask; public class TimeTest extiende TimeTask {String private JobName = ""; public TimeTest (String JobName) {super (); this.JobName = JobName; } @Override public void run () {system.out.println ("ejecutar" + jobName); } public static void main (string [] args) {Timer Timer = new Timer (); retraso largo1 = 1 * 1000; Período largo1 = 1000; // Después de 1 segundo a partir de ahora, ejecute Job1 Timer.schedule (nuevo TimeTest ("Job1"), Delay1, Period1); retraso largo2 = 2 * 1000; Período largo2 = 2000; // Después de 2 segundos a partir de ahora, ejecute Job2 Timer.schedule (nuevo TimeTest ("Job2"), Delay2, Period2); }} /**
Resultado de salida:
Ejecutar Job1
Ejecutar Job1
Ejecutar Job2
Ejecutar Job1
Ejecutar Job1
Ejecutar Job2
*/
Las clases centrales que usan temporizador para implementar la programación de tareas son Temperador y TimeTask. El temporizador es responsable de establecer el tiempo de ejecución de inicio e intervalo de TimeTask. El usuario solo necesita crear una clase de herencia de TimeTkask, implementar su propio método de ejecución y luego lanzarlo al temporizador para su ejecución. El núcleo de diseño del temporizador es una lista de tareas y un taller. El temporizador arroja las tareas recibidas a su lista de tareas, que clasifica la lista de tareas de acuerdo con el tiempo de ejecución inicial de la tarea. TimerThread comienza a convertirse en un hilo de demonio al crear un temporizador. Este hilo encuesta todas las tareas, encuentra una tarea que recientemente se ejecutará y luego duerme. Cuando la hora de inicio de la más recientemente se ejecutará, TimeRThread se despierta y se ejecuta la tarea. Después de eso, TimerThread actualiza la tarea más reciente que se ejecutará y continúa hibernando.
La ventaja del temporizador es que es simple y fácil de usar, pero dado que todas las tareas están programadas para el mismo hilo, todas las tareas se ejecutan en serie y solo una tarea se puede ejecutar al mismo tiempo. El retraso o excepción de la tarea anterior afectará las tareas posteriores (este punto debe prestarse atención).
ProgramedExecutor
En vista de las deficiencias anteriores del temporizador, Java 5 ha lanzado ProgramedExecutor basado en el diseño del grupo de subprocesos. La idea de diseño es que cada tarea programada será ejecutada por un hilo en el grupo de subprocesos, por lo que las tareas se ejecutan simultáneamente y no serán alteradas entre sí. Cabe señalar que ProplingExecutor solo iniciará un hilo cuando llegue la hora de ejecución de la tarea, y el ProjectedExecutor está encuestando la tarea por el resto del tiempo.
paquete com.ibm.scheduler; import java.util.concurrent.executors; import java.util.concurrent.scheduledExecutorservice; import java.util.concurrent.timeunit; clase pública programada conutortest implementa runnable {private string Jobname = ";"; public scheduledExecutortest (String JobName) {super (); this.JobName = JobName; } @Override public void run () {system.out.println ("ejecutar" + jobName); } public static void main (string [] args) {Servicio ProchuledExecutorService = Ejecutors.NeWSChedulEdThreadPool (10); Long InitialDelay1 = 1; Período largo1 = 1; // Después de 1 segundo a partir de ahora, ejecute Job1 Service.schedleatFixedRate (nuevo ProgramedExecutortest ("Job1"), InitialDelay1, Pasion1, TimeUnit.Seconds); Long InitialDelay2 = 1; retraso largo2 = 1; // Después de 2 segundos a partir de ahora, ejecute Job2 Service.SchedulewithFixedDelay (nuevo ProgramedExecutortest ("Job2"), InitialDelay2, Delay2, TimeUnit.Seconds); }}/** Resultado de salida: ejecute Job1Execute Job1Execute Job2Execute Job1Execute Job1Execute Job2*/
El código anterior muestra dos métodos de programación más utilizados, ScheduleAtFixedRate y SchedulewithFixedDelay, en Programedelta. ScheduleAtFixedRate Each execution time is a time interval pushing back from the beginning of the previous task, that is, each execution time is: initialDelay, initialDelay+period, initialDelay+2*period, … ScheduleWithFixedDelay Each execution time is a time interval pushing back from the end of the previous task, that is, each execution time is: initialDelay, InitialDelay+ExecuteTime+Retraso, InitialDelay+2*ExecuteTime+2*Retraso. Se puede ver que ScheduleAtFixedRate se basa en intervalos de tiempo fijos para la programación de tareas, y SchedulewithImedDelay depende de la duración de cada tiempo de ejecución de tareas y se basa en intervalos de tiempo no fijos para la programación de tareas.
Use ProchuledExecutor y calendario para implementar una programación de tareas complejas
Tanto Timer como ProchuledExecutor solo pueden proporcionar una programación de tareas basada en el intervalo de tiempo y repetición de inicio, y no son competentes para requisitos de programación más complejos. Por ejemplo, establezca la tarea para ejecutarse todos los martes a las 16:38:10. Esta función no se puede implementar directamente utilizando Timer o ProchuledExecutor, pero podemos implementarla indirectamente con la ayuda del calendario.
paquete com.ibm.scheduler; import java.util.calendar; import java.util.date; import java.util.timerTask; import java.util.concurrent.executors; import java.util.concurrent.scheduledexecutorservice; import java.util.concurrent.timit; extiende TimeTask {String private JobName = ""; public SCRODELEDEXCEUTORT2 (String JobName) {super (); this.JobName = JobName; } @Override public void run () {system.out.println ("date ="+new Date ()+", ejecutar"+JobName); } /** * Calculate the latest time that starts from the current time currentDate and meets the conditions dayOfWeek, hourOfDay, * minuteOfHour, secondOfMinite* @return */ public Calendar getEarliestDate(Calendar currentDate, int dayOfWeek, int hourOfDay, int minuteOfHour, int secondOfMinite) { //Calculate the values of WEEK_OF_YEAR, DAY_OF_WEEK, Hour_of_day, Minute, Second, etc. int centRentWeekOfyear = currentDate.get (calendar.week_of_year); int centreDayofweek = currentDate.get (calendar.day_of_week); int curtHour = currentDate.get (calendar.hour_of_day); int centreMinute = currentDate.get (calendar.minute); int corralsecond = currentDate.get (calendar.second); // Si el Dayofweek en la condición de entrada es menor que el día de la fecha actual, entonces Week_OF_Year debe posponerse por una semana boolean weeklater = false; if (dayofweek <currentdayofweek) {weeklater = true; } else if (dayofweek == currentdayofweek) {// Cuando la condición de entrada es igual al día de la fecha actual, si // horafday en la condición de entrada es menor que // actual de la fecha actual, week_of_year debe posponerse por una semana si (hora del día <currentHour) {weeMAlater = verdadero; } else if (HourOfday == Currenthour) {// Cuando la condición de entrada es igual al DayOfWeek, hora de la fecha actual, // si el minuto de la condición de entrada es menor que // actual minuto de la fecha actual, week_of_year debe posponerse por una semana si ((minuto de corriente de corriente) {semanario = verdadero; Verdadero; } else if (minutoFhour == CurrentSecond) {// Cuando la condición de entrada es igual a Dayofweek, HourOfday, // MinuteOfHour, IF // SecondOfminite en la condición de entrada es menor que el segundo lugar de la fecha actual, // luego Week_Year debe pospuesto por una semana si (SecondOfMinite <CurrentSecond) {Weadylater = verdadero verdadero. }}}}} if (weeklater) {// set week_of_year en la fecha actual para posponer una semana CurrentDate.set (calendar.week_of_year, currentweekofyear + 1); } // establecer Day_of_week, Hour_of_day, Minute, segundo en la fecha actual como los valores en la condición de entrada. currentDate.set (calendar.day_of_week, dayofweek); currentDate.set (calendar.hour_of_day, horafday); CurrentDate.set (calendario.minute, minutofhour); CurrentDate.set (calendar.second, segundofminita); devolver CurrentDate; } public static void main (string [] args) lanza la excepción {ProgramedExCeUtTest2 test = new ProchuledExCeUtTest2 ("Job1"); // Obtenga el calendario de tiempo actual CurrentDate = calendar.getInstance (); Long CurrentDateLong = CurrentDate.GetTime (). GetTime (); System.out.println ("Current Date =" + CurrentDate.GetTime (). ToString ()); // Calcule el último tiempo de ejecución que cumple con el calendario de condición más temprano = test .getearliestDate (CurrentDate, 3, 16, 38, 10); mucho antes datelong = EarlyDate.GetTime (). GetTime (); System.out.println ("Fecha más temprana =" + EarlyDate.gettime (). ToString ()); // Calcule el intervalo de tiempo desde la hora actual hasta el último tiempo de ejecución Long Delay = EarlyDatelong - CurrentDateLong; // Calcule el período de ejecución es un período de largo período de larga duración = 7 * 24 * 60 * 60 * 1000; Servicio ProchuledExecutorService = Ejecutores.NewScheduledThreadPool (10); // a partir de ahora retrasar milisegundos, ejecutar el servicio Job1.schedleatFixedRate (prueba, retraso, período, timilliseConds); }}/** Resultado de la salida: fecha actual = miércoles 02 de febrero 17:32:01 CST 2011 ALEGIEST Date = martes 8 de febrero 16:38:10 CST 2011 Date = martes 8 de febrero 16:38:10 CST 2011, ejecute Job1Date = martes 15 de febrero 16:38:10 CST 2011, ejecutive Job1*/
El código anterior implementa la función de programar tareas a las 16:38:10 todos los martes. El núcleo es calcular el tiempo absoluto de 16:38:10 el último martes en función de la hora actual, y luego calcular la diferencia de tiempo de la hora actual como un parámetro para llamar a la función ProgramedExCeutor. Para calcular el último tiempo, se usa la función de java.util.calendar. Primero, necesitamos explicar algunas ideas de diseño del calendario. El calendario tiene las siguientes combinaciones que identifican de manera única una fecha:
Cita
Año + mes + day_of_month
Año + mes + week_of_month + day_of_week
Año + mes + day_of_week_in_month + day_of_week
Año + day_of_year
Año + day_of_week + week_of_year
Las combinaciones anteriores se combinan con HourOfday + Minute + Second para ser una marca de tiempo completa.
La demostración anterior adopta el último método de combinación. La entrada es Day_of_Week, Hour_of_day, Minute, Second y la fecha actual, y la salida es una fecha futura que satisface Day_of_Week, Hour_of_day, Minute, Second y está más cerca de la fecha actual. El principio de cálculo es comenzar la comparación de la entrada Day_of_week. Si es menos que el día_of_week de la fecha actual, debe aumentar aún más la semana_of_year, es decir, agregar la semana_of_year en la fecha actual y sobrescribir el valor anterior; Si es igual al día actual_of_week, continúe comparando Hour_of_day; Si es mayor que el día actual_of_week, llame directamente a la función Calendar.set (campo, valor) de java.util.calenda para asignar el day_of_week, hora_of_day, minuto, segundo al valor de entrada, y así sucesivamente hasta que se llegue a la comparación a la segunda. Podemos seleccionar diferentes combinaciones basadas en los requisitos de entrada para calcular el último tiempo de ejecución.
Es bastante engorroso implementar la programación de tareas utilizando el método anterior. Esperamos necesitar una herramienta de programación de tareas más completa para resolver estos complejos problemas de programación. Afortunadamente, el cuarzo de kit de herramientas de código abierto ha mostrado grandes capacidades a este respecto.
Cuarzo
OpenSymphony Open Source Organization es otro proyecto de código abierto en el campo de la programación de empleo, que se puede combinar con aplicaciones J2EE y J2SE o usarse solo. El cuarzo se puede usar para crear programas simples o complejos que ejecutan diez, cientos o incluso decenas de miles de empleos.
Echemos un vistazo a un ejemplo:
paquete com.test.quartz; import static org.quartz.dateBuilder.newdate; import static org.quartz.jobbuilder.newjob; import static org.quartz.simplechedulebuilder.simplechedule; import static org.quartz.triggerBuilder.newtrigger; importar Java.utilEcal. org.quartz.jobdetail; import org.quartz.scheduler; import org.quartz.rigger; import org.quartz.impl.stdschedulerFactory; import org.quartz.impl.calendar.annualCalendar; public class QuartzTest {public voides main (] string [] programador = stdschedulerFactory.getDefaultScheduler (); // Defina un activador de activación desencadenante = newTrigger (). WithIdentity ("Trigger1", "Group1") // Define Name/Group .startnow () // Una vez que se agrega Scheduler, toma efecto inmediatamente. hasta que todavía no se detenga. // Defina una JobDetail JobDetail Job = NewJob (Helloquartz.Class) // Defina la clase de trabajo como Helloquartz Class, que es la Logic de ejecución real. // Agregar a este scheduler.scheduleJob (trabajo, disparador); // Iniciar Scheduler.Start (); // hilo cerrado. Sleep (10000); Scheduler.shutdown (verdadero); } catch (Exception e) {E.PrintStackTrace (); }}} paquete com.test.quartz; import java.util.date; import org.quartz.disallowconcurrentExecution; import org.quartz.job; import org.quartz.jobdetail; importar org.quartz.JobExecution; Void Execute (JobExecutionContext Context) lanza JobExecutionException {JobDetail Detalle = context.getJobDetail (); Name de cadena = detall.getJobDatamap (). GetString ("nombre"); System.out.println ("saluda a" + name + "en" + nueva fecha ()); }}A través de los ejemplos anteriores: los 3 elementos básicos más importantes del cuarzo:
• Scheduler: Scheduler. Toda la programación está controlada por él.
• Disparador: Define la condición de activación. En el ejemplo, su tipo es SimpleTrigger, que se ejecuta cada 1 segundo (lo que es SimpleTrigger, se describirá en detalle a continuación).
• JobDetail y trabajo: JobDetail define los datos de la tarea, y la lógica de ejecución real está en el trabajo, en el ejemplo, Helloquartz. ¿Por qué está diseñado como un trabajo de JobDetail +, y no utiliza directamente el trabajo? Esto se debe a que es posible ejecutar tareas simultáneamente. Si Scheduler usa trabajos directamente, habrá un problema de acceso concurrente a la misma instancia de trabajo. En el método de trabajo de trabajo y trabajo, cada vez que se ejecute el sheduler, creará una nueva instancia de trabajo basada en la cola de trabajo, para que se pueda evitar el problema del acceso concurrente.
API de cuarzo
El estilo API de cuarzo es después de 2.x, y adopta el estilo DSL (generalmente significa estilo de interfaz fluido), que es la parte NewTrigger () en el ejemplo. Se implementa a través de Builder, que es lo siguiente. (La mayoría de los siguientes códigos se refieren a estos constructores)
// Builderport estatitario relacionado con el trabajo Org.quartz.JobBuilder.*; // Builderport en el gatillo Org.quartz.triggerBuilder.*; import static org.quartz.SimpleCheduleBuilder.*; importar org.quartz.cronscheduleBuilder.*; import static static. org.quartz.DailyTimeIntervalScheduleBuilder.*; Importar static org.quartz.calendarintervalscheduleBuilder.*; // Date Builderport Static Org.quartz.dateBuilder.*; El estilo DSL será más coherente y refrescante de escribir, y ya que no es el estilo de uso de Setter, será más fácil de comprender, será más fácil de comprender. Comparar: JobDetail JobDetail = New JobDetailImpl ("JobDetail1", "Group1", Helloquartz.Class); JobDetail.getJobDatamap (). Put ("Name", "Quartz"); SimpleTrigGerImpl turger = new SimpleTRiggerIpl ("TRIGGR1", "Group1"); Date ()); ARGRIT.SetRepeatInterval (1); ARGRIT.SetRepeatCount (-1);Sobre el nombre y el grupo
JobDetail y Trigger tienen nombre y grupo.
El nombre es su identificador único en este cobertizo. Si queremos actualizar una definición de JobDetail, solo necesitamos establecer una instancia de JobDetail con el mismo nombre.
Group es una unidad de organización, y Sheduler proporcionará algunas API para todo el grupo de operaciones, como Scheduler.ResumeJobs ().
Desencadenar
Antes de comenzar a explicar cada disparador en detalle, debe comprender algunos puntos en común del desencadenante.
Inicio Time & End Time
El intervalo de tiempo especificado por Starttime y EndTime se activará. Fuera de este intervalo, el disparador no se activará. Todos los desencadenantes contendrán estas dos propiedades.
Prioridad
Cuando el planificador está ocupado, se pueden activar múltiples desencadenantes al mismo tiempo, pero los recursos son insuficientes (como un grupo de hilos insuficiente). Luego, en este momento, una mejor manera que una tela de piedra de tijeras es establecer prioridad. Ejecute la prioridad primero. Cabe señalar que la prioridad solo funcionará entre los desencadenantes ejecutados al mismo tiempo, si un desencadenante es a las 9:00 y el otro disparador es a las 9:30. Entonces, no importa cuán alta sea la próxima prioridad, la anterior se ejecutará primero. El valor predeterminado de la prioridad es 5, y el valor predeterminado se usa cuando es negativo. El valor máximo no parece especificarse, pero se recomienda seguir el estándar Java y usar 1-10. De lo contrario, si sabe si hay un valor mayor cuando ve [la prioridad es 10].
Estrategia de fallo de fuego (Miss Trigger)
Cuando los recursos de programador similares son insuficientes, o cuando la máquina se bloquea y se reinicia, es posible que algunos desencadenantes no se activen en el momento en que deben activarse, es decir, perder el fuego. En este momento, Trigger necesita una estrategia para manejar esta situación. Las estrategias opcionales para cada disparador varían. Aquí hay dos puntos para prestar atención a:
Disparado de firo tiene un valor umbral, que está configurado en la tienda de trabajo. Más que Ramjobstore es org.quartz.jobstore.misfirethreshold. El fallo de falla solo se contará como excediendo este umbral. Menos que este umbral, el cuarzo se volverá a retirarse. Todas las estrategias de fallo de falla en realidad responden dos preguntas:
• ¿Eso tiene un fallo de falla tiene que volver a actuar?
• Si se produce un fallo de fallo, ¿desea ajustar el tiempo de programación existente?
Por ejemplo, la estrategia de falso de SimpleTrigger incluye:
• Disfire_instruction_ignore_misfire_policy Esto no significa ignorar el disparador perdido, sino ignorar la política de fallo. Recuperará todas las tareas de fallo de fallo cuando el recurso sea apropiado y no afectará el tiempo de programación existente. Por ejemplo, SimpleTrigger se ejecuta cada 15 segundos, y tiene 5 minutos en el medio y faltan 20 falta. Después de 5 minutos, suponiendo que los recursos son suficientes y la tarea permite la concurrencia, se activará a la vez. Esta propiedad es aplicable a todos los desencadenantes.
• Misfire_instruction_fire_now Ignore tareas que han sido malas severas y realizan horarios de inmediato. Esto generalmente solo se aplica a las tareas que solo se realizan una vez.
• Misfire_instruction_reschedule_now_with_existing_repeat_count Establezca la hora de inicio en la hora actual e inmediatamente reprograma la tarea, incluida la falla.
• Misfire_instruction_reschedule_now_with_remining_repeat_count Similar a la incendio de incendio deReschedulenowwithexistingRepeat_count, la diferencia es que las tareas que ya han sido malas se ignoran.
• Misfire_instruction_reschedule_next_with_existing_count en la siguiente hora programada, reinicie la tarea de despacho, incluida la falla de falso.
• Misfire_instruction_reschedule_next_with_remining_count Similar a la incendio de incendio deReschedulenExtwithexistingCount, la diferencia es que las tareas que ya han sido maltratadas serán ignoradas.
• Misfire_instruction_smart_policy Todos los valores predeterminados de fallo de activación son este, lo que significa aproximadamente "deje la lógica de procesamiento en el cuarzo inteligente para decidir". La estrategia básica es.
• Si el horario solo se ejecuta una vez, use Misfire_instruction_Fire_Now.
• Si se trata de una programación infinita (RepeplEfunt es infinito), use Misfire_instruction_Reschedule_Next_With_Remaining_Count.
• De lo contrario, usar Misfire_instruction_Reschedule_Now_With_Existing_Repeat_Count Effire es bastante complicado, puede consultar este artículo.
Calendario
El calendario aquí no es el java.util.calendar de JDK, no para calcular las fechas. Su función es complementar el tiempo del disparador. Se pueden excluir o agregar ciertos puntos específicos en el tiempo.
Tomando "el reembolso automático de las deudas de tarjetas a las 0:00 el 25 de cada mes" como ejemplo, queremos descartar el punto de tiempo del 25 de febrero de cada año (porque hay 2.14, febrero definitivamente se declarará en bancarrota). Este tiempo puede ser logrado por el calendario.
ejemplo:
Anualcalendar cal = new AnnualCalendar (); // Definir un calendario ejecutado cada año con una precisión de días, es decir, no se puede definir hasta las 2:00 pm en 2.25 java.util.calendar exclueday = new Gregoriancalendar (); Excludeday.setTime (NewDate (). Inmediation (2, 25) .Build ()); cal.setayxdayxclas // establecer la fecha de exclusión 2.25 scheduler.addcalendar ("febcal", cal, falso, falso); // Scheduler se une a este calendario // Defina un trigrigger trigger = newTrigger (). WithIdentity ("Trigger1", "Group1") .StartNow () // Una vez que se agrega el programador, entra en vigencia inmediatamente. .WithSchedule (simplesChedule () .WithIntervalInseconds (1) .RepeatForever ()) .Build ();Quartz nos proporciona considerablemente los siguientes calendarios. Tenga en cuenta que todos los calendarios pueden excluir o inclusión dependiendo de:
• HolidayCalendar. Especifique una fecha específica, como 20140613. Precisión al cielo.
• Daily Calendar. Especifica el período de tiempo de cada día (RangeStartingTime, RangeendingTime), el formato es HH: MM [: SS [: MMMM]]. Es decir, la máxima precisión puede llegar a milisegundos.
• Weeklycalendar. Especifique el día de la semana de cada semana, el valor opcional es java.util.calendar.sunday. La precisión es día.
• Monthlycalendar. Especifique el día de cada mes. Los valores opcionales son 1-31. La precisión es día
• Anual Calendar. Especifique qué día del año. El método de uso es como se muestra en el ejemplo anterior. La precisión es día.
• Croncalendar. Especifica una expresión cron. La precisión depende de la expresión cron, es decir, la máxima precisión puede alcanzar segundos.
Clase de implementación de activación
Quartz tiene las siguientes implementaciones de activación:
Sencillo
Especifica tareas que comienzan en un momento determinado y se realizan en un cierto intervalo de tiempo (en milisegundos). Es adecuado para tareas similares a: comenzar a las 9:00 y ejecutar cada 1 hora. Sus propiedades son:
• Repetir intervalo de repetición
• Repita el número de repeticiones. El número real de ejecuciones es repetir+1. Porque se ejecutará una vez en la hora de inicio. Lo mismo se aplica a la propiedad repetida a continuación.
ejemplo:
CalendarIntervalTrigger
Similar a SimpleTrigger, especifica tareas que comienzan en un momento determinado y se ejecutan en un cierto intervalo de tiempo. Pero la diferencia es que el intervalo de tiempo especificado por SimpleTrigger es milisegundos, y no hay forma de especificar que se ejecutará cada dos meses (el intervalo mensual no es un valor fijo), mientras que las unidades de intervalo respaldadas por CalendarIntervalTrager incluyen segundos, minutos, horas, días, años, años y semanas. En comparación con SimpleTrigger, tiene dos ventajas: 1. Es más conveniente. Por ejemplo, si ejecuta cada 1 hora, no tiene que calcular cuántos milisegundos 1 hora es igual. 2. Admite intervalos que no son de longitud fija, como intervalos que son meses y años. Pero la desventaja es que la precisión solo puede alcanzar segundos. Su tarea adecuada es similar a: comenzar a las 9:00 y ejecutar una vez cada semana a las 9:00. Sus propiedades son:
• Intervalo de ejecución del intervalo
• Unidades de intervaluga del intervalo de ejecución (segundos, minutos, horas, días, meses, años, semanas)
ejemplo:
calendarIntervalSchedule () .withintervalIndays (1) // ejecutar una vez al día.Build (); calendarIntervalSchedule () .WithintervalInweeks (1) // ejecutar una vez a la semana.Build ();
DailyTimeIntervalTrigger
Especifique que las tareas se realicen a ciertos intervalos durante un cierto período de tiempo todos los días. Y puede admitir semanas especificadas. Su tarea adecuada es similar a: Especificar de 9:00 a 18:00 todos los días, ejecutada cada 70 segundos y solo de lunes a viernes. Sus propiedades son:
• Inicio de la hora de inicio del día de inicio todos los días
• Tiempo de tiempo final de la hora del día del día
• Días de la semana que se ejecutará
• Intervalo de ejecución del intervalo
• Unidades de intervaluga del intervalo de ejecución (segundos, minutos, horas, días, meses, años, semanas)
• Repetir el número de repeticiones
ejemplo:
DailyTimeIntervalSchedule () .StarTingDailyat (TimeOfday.HourandMinuteOfday (9, 0)) // Comience a las 9:00 en el día. dentro de Tervalinhours (1) // Ejecutar cada 1 hora. WithrePeatCount (100) // repite hasta 100 veces (en realidad ejecutado 100+1 veces) .Build (); DailyTimeNintervalSchedule () .StarTingDailyat (TimeOfday.HourandMinuteOfday (9, 0)) // Comience a las 9:00 en el día. Este método en realidad calcula el fin de tiempo del día de inicio del día de inicio+intervalo*cuenta.
Crontrigger
Adecuado para tareas más complejas, admite la sintaxis escrita a Linux Cron (y es más potente). Básicamente, cubre la mayoría (pero no todos) de los tres desencadenantes anteriores; por supuesto, también es más difícil de entender. Sus tareas adecuadas son similares a: cada una una vez realizada una vez al día a las 0:00, 9:00 y 18:00. Sus propiedades son solo:
Expresiones cron
Pero esta representación en sí es lo suficientemente compleja. Habrá instrucciones a continuación. ejemplo:
cronschedule ("0 0/2 8-17 * * *?") // Ejecutar cada 2 minutos a las 8: 00-17: 00 todos los días.Build (); cronschedule ("0 30 9? * Mon") // ejecutar todos los lunes a las 9: 30.build (); WeeklyDayandHourandMinute (lunes 9, 30) // equivalente a 0 30 9 9? * Mon .Build ();Expresiones cron
| Ubicación | Dominio de tiempo | Valores permitidos | Valor especial |
| 1 | Segundo | 0-59 | , - * / |
| 2 | minuto | 0-59 | , - * / |
| 3 | Hora | 0-23 | , - * / |
| 4 | fecha | 1-31 | , - *? / LWC |
| 5 | mes | 1-12 | , - * / |
| 6 | Semana | 1-7 | , - *? / LC # |
| 7 | Año (opcional) | 1-31 | , - * / |
• Asterisk (): se puede usar en todos los campos para representar cada momento en el dominio de tiempo correspondiente, por ejemplo, en el campo minuto, significa "por minuto";
• Marque de interrogación (?): Este personaje solo se usa en los campos de la fecha y la semana, y generalmente se especifica como un "valor sin sentido", equivalente a un carácter DOT;
• Mínimo signo (-): expresa un rango. Si se usa "10-12" en el campo de la hora, significa de 10 a 12 puntos, es decir, 10,11,12;
• Coma (,): expresa un valor de lista. Si "Mon, Wed, Vie" se usa en el campo de la semana, significa lunes, miércoles y viernes;
• Slash (/): X/Y representa una secuencia de igual paso, x es el valor inicial e y es el valor de paso incremental. Si usa 0/15 en el campo de minuto, se expresa como 0, 15, 30 y 45 segundos, mientras que 5/15 significa 5, 20, 35, 50 en el campo de minuto, también puede usar */y, que es equivalente a 0/y;
• L: Este personaje solo se usa en los campos de fecha y semana, lo que representa el significado de "último", pero significa de manera diferente en los dos campos. L En el campo de la fecha indica el último día del mes, como el 31 de enero y el 28 de febrero, que no es un año salto; Si L se usa en la semana, indica el sábado, que es equivalente a 7. Sin embargo, si L aparece en el campo de la semana y está precedido por un valor X, significa "los últimos x días del mes", por ejemplo, 6L significa el último viernes del mes;
• W: Este personaje solo puede aparecer en el campo de la fecha y es una modificación de la fecha principal, lo que indica la jornada laboral más cercana a la fecha. Por ejemplo, 15W representa el día de trabajo más cercano al 15 del mes. Si el 15 del mes es el sábado, coincide el viernes 14; Si el 15 del mes es el domingo, coincide el lunes 16; Si el 15 del mes es el martes, es el martes 15. Sin embargo, debe tenerse en cuenta que la fecha de correspondencia asociada no se puede cruzar al mes. Si especifica 1W, si el primer día es el sábado, el resultado coincide el lunes 3, no el último día del último mes. Una cadena W solo puede especificar una sola fecha, pero no puede especificar un rango de fecha;
• Combinación LW: LW se puede usar en el campo de fecha, lo que significa el último día hábil del mes; Pound Sign (#): este personaje solo se puede usar en el campo de la semana, que representa un día hábil del mes. Por ejemplo, 6#3 representa el tercer viernes del mes (6 representa el viernes,#3 representa el tercero en este momento), mientras que 4#5 representa el quinto miércoles del mes, suponiendo que el mes no tiene el quinto miércoles, se ignora y no se desencadena;
• C: Este personaje solo se usa en los campos de fecha y semana, lo que representa el significado de "calendario". Significa la fecha asociada con el plan, y si la fecha no está asociada, es equivalente a todas las fechas en el calendario. Por ejemplo, 5C en el campo de la fecha es equivalente al primer día después del quinto día del calendario. 1C es equivalente al primer día después del domingo en el campo de la semana.
Las expresiones cron no son sensibles al caso de los caracteres especiales y no son sensibles a la abreviatura del caso inglés de la semana. Algunos ejemplos:
| Expresión | ilustrar |
| 0 0 12 * *? | Corre a las 12 en punto todos los días |
| 0 15 10? * * | Correr a las 10:15 todos los días |
| 0 15 10 * *? | Correr a las 10:15 todos los días |
| 0 15 10 * *? * | Correr a las 10:15 todos los días |
| 0 15 10 * *? 2008 | Correr a las 10:15 al día en 2008 |
| 0 * 14 * *? | Corre cada minuto entre las 14:00 y termina a las 14:59 todos los días. |
| 0 0/5 14 * *? | Corre cada 5 minutos de 14:00 a 15:00 todos los días, comenzando a las 14:55 y terminando a las 14:55. |
| 0 0/5 14,18 * *? | Se ejecuta cada 5 minutos de 14:00 a 15:00 todos los días, y funciona cada 5 minutos de 18:00 a 19:00 todos los días. |
| 0 0-5 14 * *? | Corre cada minuto de 14:00 a 14:05 todos los días. |
| 0 10,44 14? 3 miércoles | Corre una vez por minuto todos los miércoles de 14:10 a 14:44. |
| 0 15 10? * Mon-fri | Corre todos los lunes, martes, miércoles, jueves, jueves y viernes a las 10:15. |
| 0 15 10 15 * ? | 每月15日10:15分运行。 |
| 0 15 10 L * ? | 每月最后一天10:15分运行。 |
| 0 15 10 ? * 6L | 每月最后一个星期五10:15分运行。 |
| 0 15 10 ? * 6L 2007-2009 | 在2007,2008,2009年每个月的最后一个星期五的10:15分运行。 |
| 0 15 10 ? * 6#3 | 每月第三个星期五的10:15分运行。 |
JobDetail & Job
JobDetail是任务的定义,而Job是任务的执行逻辑。在JobDetail里会引用一个Job Class定义。一个最简单的例子:
public class JobTest { public static void main(String[] args) throws SchedulerException, IOException { JobDetail job=newJob() .ofType(DoNothingJob.class) //Check Job Class .withIdentity("job1", "group1") //Set name/group .withDescription("this is a test job") //Set description.usingJobData("age", 18) //Add attributes to ageJobDataMap .build(); job.getJobDataMap().put("name", "quertz"); //Add attribute name to JobDataMap //Define a SimpleTrigger trigger that executes once per second. Trigger trigger=newTrigger().startNow().withIdentity("trigger1") .withSchedule(simpleSchedule().withIntervalInSeconds(1) .repeatForever()) .build(); Scheduler sche=StdSchedulerFactory.getDefaultScheduler(); sche.scheduleJob(job, trigger); sche.start(); System.in.read(); sche.shutdown(); }}public class DoNothingJob implements Job { public void execute(JobExecutionContext context) throws JobExecutionException { System.out.println("do nothing"); }}从上例我们可以看出,要定义一个任务,需要干几件事:
•创建一个org.quartz.Job的实现类,并实现实现自己的业务逻辑。比如上面的DoNothingJob。
•定义一个JobDetail,引用这个实现类
•加入scheduleJob Quartz调度一次任务,会干如下的事:
•JobClass jobClass=JobDetail.getJobClass()
•Job jobInstance=jobClass.newInstance()。所以Job实现类,必须有一个public的无参构建方法。
•jobInstance.execute(JobExecutionContext context)。JobExecutionContext是Job运行的上下文,可以获得Trigger、Scheduler、JobDetail的信息。
也就是说,每次调度都会创建一个新的Job实例,这样的好处是有些任务并发执行的时候,不存在对临界资源的访问问题――当然,如果需要共享JobDataMap的时候,还是存在临界资源的并发访问的问题。
JobDataMap
Job是newInstance的实例,那我怎么传值给它? 比如我现在有两个发送邮件的任务,一个是发给"liLei",一个发给"hanmeimei",不能说我要写两个Job实现类LiLeiSendEmailJob和HanMeiMeiSendEmailJob。实现的办法是通过JobDataMap。
每一个JobDetail都会有一个JobDataMap。JobDataMap本质就是一个Map的扩展类,只是提供了一些更便捷的方法,比如getString()之类的。
我们可以在定义JobDetail,加入属性值,方式有二:
•newJob().usingJobData("age", 18) //加入属性到ageJobDataMap
•job.getJobDataMap().put("name", "quertz"); //加入属性name到JobDataMap
然后在Job中可以获取这个JobDataMap的值,方式同样有二:
public class HelloQuartz implements Job { private String name; public void execute(JobExecutionContext context) throws JobExecutionException { JobDetail detail = context.getJobDetail(); JobDataMap map = detail.getJobDataMap(); //Method 1: Obtain JobDataMap System.out.println("say hello to " + name + "[" + map.getInt("age") + "]" + " at " + new Date()); } //Method 2: The setter method of the property will automatically inject the JobDataMap attribute into public void setName(String name) { this.name = name; }}对于同一个JobDetail实例,执行的多个Job实例,是共享同样的JobDataMap,也就是说,如果你在任务里修改了里面的值,会对其他Job实例(并发的或者后续的)造成影响。
除了JobDetail,Trigger同样有一个JobDataMap,共享范围是所有使用这个Trigger的Job实例。
Job并发
Job是有可能并发执行的,比如一个任务要执行10秒中,而调度算法是每秒中触发1次,那么就有可能多个任务被并发执行。
Sometimes we don’t want to execute tasks concurrently, for example, this task needs to "get a list of all unsent mails in the database". If it is executed concurrently, a database lock is needed to avoid a data being processed multiple times. This time a @DisallowConcurrentExecution solves this problem. Eso es todo:
public class DoNothingJob implements Job { @DisallowConcurrentExecution public void execute(JobExecutionContext context) throws JobExecutionException { System.out.println("do nothing"); }}注意,@DisallowConcurrentExecution是对JobDetail实例生效,也就是如果你定义两个JobDetail,引用同一个Job类,是可以并发执行的。
JobExecutionException
Job.execute()方法是不允许抛出除JobExecutionException之外的所有异常的(包括RuntimeException),所以编码的时候,最好是try-catch住所有的Throwable,小心处理。
Otras propiedades
•Durability(耐久性?) 如果一个任务不是durable,那么当没有Trigger关联它的时候,它就会被自动删除。
•RequestsRecovery 如果一个任务是"requests recovery",那么当任务运行过程非正常退出时(比如进程崩溃,机器断电,但不包括抛出异常这种情况),Quartz再次启动时,会重新运行一次这个任务实例。
可以通过JobExecutionContext.isRecovering()查询任务是否是被恢复的。
Scheduler
•Scheduler就是Quartz的大脑,所有任务都是由它来设施。
•Schduelr包含一个两个重要组件: JobStore和ThreadPool。
•JobStore是会来存储运行时信息的,包括Trigger,Schduler,JobDetail,业务锁等。它有多种实现RAMJob(内存实现),JobStoreTX(JDBC,事务由Quartz管理),JobStoreCMT(JDBC,使用容器事务),ClusteredJobStore(集群实现)、TerracottaJobStore(什么是Terractta)。
•ThreadPool就是线程池,Quartz有自己的线程池实现。所有任务的都会由线程池执行。
SchedulerFactory
SchdulerFactory,顾名思义就是来用创建Schduler了,有两个实现:DirectSchedulerFactory和StdSchdulerFactory。前者可以用来在代码里定制你自己的Schduler参数。后者是直接读取classpath下的quartz.properties(不存在就都使用默认值)配置来实例化Schduler。通常来讲,我们使用StdSchdulerFactory也就足够了。
SchdulerFactory本身是支持创建RMI stub的,可以用来管理远程的Scheduler,功能与本地一样,可以远程提交个Job什么的。DirectSchedulerFactory的创建接口:
/** * Same as * {@link DirectSchedulerFactory#createScheduler(ThreadPool threadPool, JobStore jobStore)}, * with the addition of specifying the scheduler name and instance ID. This * scheduler can only be retrieved via * {@link DirectSchedulerFactory#getScheduler(String)} * * @param schedulerName * The name for the scheduler. * @param schedulerInstanceId * The instance ID for the scheduler. * @param threadPool * The thread pool for executing jobs * @param jobStore * The type of job store * @throws SchedulerException * if initialization failed */ public void createScheduler(String schedulerName, String schedulerInstanceId, ThreadPool threadPool, JobStore jobStore) throws SchedulerException;StdSchdulerFactory的配置例子, 更多配置,参考Quartz配置指南:
org.quartz.scheduler.instanceName = DefaultQuartzSchedulerorg.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPoolorg.quartz.threadPool.threadCount = 10 org.quartz.threadPool.threadPriority = 5org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = trueorg.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
三、Quartz 集成Spring
开发一个job类,普通java类,需要有一个执行的方法:
package com.tgb.lk.demo.quartz;import java.util.Date;public class MyJob { public void work() { System.out.println("date:" + new Date().toString()); }}把类放到spring容器中,可以使用配置也可以使用注解:
<bean id="myJob" />
配置jobDetail,指定job对象:
<!-- 配置jobDetail,指定job对象--> <bean id="accountJobDetail"> <property name="targetObject"> <ref bean="accountJob" /> </property> <property name="targetMethod"> <value>work</value> </property> </bean>
配置一个trigger,需要指定一个cron表达式,指定任务的执行时机:
<!-- accountTrigger 的配置--> <bean id="accountTrigger" > <property name="jobDetail"> <ref bean="accountJobDetail" /> </property> <property name="cronExpression"> <value>0/3 * * * * ?</value> </property> </bean>
配置调度工厂:
<!-- 启动触发器的配置开始--> <bean name="startQuertz" lazy-init="false" autowire="no" > <property name="triggers"> <list> <ref bean="myJobTrigger" /> </list> </property> </bean> <!-- 启动触发器的配置结束-->
项目启动,定时器开始执行。
四、分析不同定时任务优缺点,寻找一种符合你项目需求的定时任务Timer管理延时任务的缺陷
以前在项目中也经常使用定时器,比如每隔一段时间清理项目中的一些垃圾文件,每隔一段时间进行日志清理;然而Timer是存在一些缺陷的,因为Timer在执行定时任务时只会创建一个线程,所以如果存在多个任务,且任务时间过长,超过了两个任务的间隔时间,会发生一些缺陷
Timer当任务抛出异常时的缺陷
如果TimerTask抛出RuntimeException,Timer会停止所有任务的运行
Timer执行周期任务时依赖系统时间
Timer执行周期任务时依赖系统时间,如果当前系统时间发生变化会出现一些执行上的变化,ScheduledExecutorService基于时间的延迟,不会由于系统时间的改变发生执行变化。
对异常的处理
Quartz的某次执行任务过程中抛出异常,不影响下一次任务的执行,当下一次执行时间到来时,定时器会再次执行任务;而TimerTask则不同,一旦某个任务在执行过程中抛出异常,则整个定时器生命周期就结束,以后永远不会再执行定时器任务。
精确到和功能
Quartz每次执行任务都创建一个新的任务类对象,而TimerTask则每次使用同一个任务类对象。 Quartz可以通过cron表达式精确到特定时间执行,而TimerTask不能。Quartz拥有TimerTask所有的功能,而TimerTask则没有上述,基本说明了在以后的开发中尽可能使用ScheduledExecutorService(JDK1.5以后)替代Timer。
五、cron 在线表达式生成器http://cron.qqe2.com/附录cron 表达式
cron表达式用于配置cronTrigger的实例。cron表达式实际上是由七个子表达式组成。这些表达式之间用空格分隔。
•Seconds (秒)
•Minutes(分)
•Hours(小时)
•Day-of-Month (天)
•Month(月)
•Day-of-Week (周)
•Year(年)
例:"0 0 12 ? * WED” 意思是:每个星期三的中午12点执行。个别子表达式可以包含范围或者列表。例如:上面例子中的WED可以换成"MON-FRI","MON,WED,FRI",甚至"MON-WED,SAT"。子表达式范围:
•Seconds (0~59)
•Minutes (0~59)
•Hours (0~23)
•Day-of-Month (1~31,但是要注意有些月份没有31天)
•Month (0~11,或者"JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV,DEC")
•Day-of-Week (1~7,1=SUN 或者"SUN, MON, TUE, WED, THU, FRI, SAT”)
•Year (1970~2099)
Cron表达式的格式:秒分时日月周年(可选)。
Field name | Allowed value | Allowed special characters ------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------
字符含义:
•*:代表所有可能的值。因此,“*”在Month中表示每个月,在Day-of-Month中表示每天,在Hours表示每小时
•-:表示指定范围。
•,:表示列出枚举值。例如:在Minutes子表达式中,“5,20”表示在5分钟和20分钟触发。
•/:被用于指定增量。例如:在Minutes子表达式中,“0/15”表示从0分钟开始,每15分钟执行一次。"3/20"表示从第三分钟开始,每20分钟执行一次。和"3,23,43"(表示第3,23,43分钟触发)的含义一样。
•?:用在Day-of-Month和Day-of-Week中,指“没有具体的值”。当两个子表达式其中一个被指定了值以后,为了避免冲突,需要将另外一个的值设为“?”。例如:想在每月20日触发调度,不管20号是星期几,只能用如下写法:0 0 0 20 * ?,其中最后以为只能用“?”,而不能用“*”。
•L:用在day-of-month和day-of-week字串中。它是单词“last”的缩写。它在两个子表达式中的含义是不同的。
•在day-of-month中,“L”表示一个月的最后一天,一月31号,3月30号。
•在day-of-week中,“L”表示一个星期的最后一天,也就是“7”或者“SAT”
•如果“L”前有具体内容,它就有其他的含义了。例如:“6L”表示这个月的倒数第六天。“FRIL”表示这个月的最后一个星期五。
•注意:在使用“L”参数时,不要指定列表或者范围,这样会出现问题。
•W:“Weekday”的缩写。只能用在day-of-month字段。用来描叙最接近指定天的工作日(周一到周五)。例如:在day-of-month字段用“15W”指“最接近这个月第15天的工作日”,即如果这个月第15天是周六,那么触发器将会在这个月第14天即周五触发;如果这个月第15天是周日,那么触发器将会在这个月第16天即周一触发;如果这个月第15天是周二,那么就在触发器这天触发。注意一点:这个用法只会在当前月计算值,不会越过当前月。“W”字符仅能在day-of-month指明一天,不能是一个范围或列表。也可以用“LW”来指定这个月的最后一个工作日,即最后一个星期五。
•# :只能用在day-of-week字段。用来指定这个月的第几个周几。例:在day-of-week字段用"6#3" or "FRI#3"指这个月第3个周五(6指周五,3指第3个)。如果指定的日期不存在,触发器就不会触发。
表达式例子:
0 * * * * ? 每1分钟触发一次
0 0 * * * ? 每天每1小时触发一次
0 0 10 * * ? 每天10点触发一次
0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发
0 30 9 1 * ? 每月1号上午9点半
0 15 10 15 * ? 每月15日上午10:15触发
*/5 * * * * ? 每隔5秒执行一次
0 */1 * * * ? 每隔1分钟执行一次
0 0 5-15 * * ? 每天5-15点整点触发
0 0/3 * * * ? 每三分钟触发一次
0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发
0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
0 0 12 ? * WED 表示每个星期三中午12点
0 0 17 ? * TUES,THUR,SAT 每周二、四、六下午五点
0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发
0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
0 0 23 L * ? 每月最后一天23点执行一次
0 15 10 L * ? 每月最后一日的上午10:15触发
0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发
0 15 10 * * ? 2005 2005年的每天上午10:15触发
0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发
0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发
The above timed tasks (example explanation) in Java implementation web applications are all the content I share with you. Espero que pueda darle una referencia y espero que pueda apoyar más a Wulin.com.