1818
1919import java .lang .reflect .Method ;
2020import java .util .HashMap ;
21+ import java .util .LinkedHashSet ;
2122import java .util .Map ;
23+ import java .util .Set ;
24+ import java .util .concurrent .ConcurrentHashMap ;
2225import java .util .concurrent .ScheduledExecutorService ;
2326
2427import org .springframework .aop .support .AopUtils ;
4043import org .springframework .util .Assert ;
4144import org .springframework .util .ReflectionUtils ;
4245import org .springframework .util .ReflectionUtils .MethodCallback ;
46+ import org .springframework .util .StringUtils ;
4347import org .springframework .util .StringValueResolver ;
4448
4549/**
@@ -78,7 +82,10 @@ public class ScheduledAnnotationBeanPostProcessor
7882
7983 private final ScheduledTaskRegistrar registrar = new ScheduledTaskRegistrar ();
8084
85+ private final Map <Class <?>, Boolean > nonAnnotatedClasses = new ConcurrentHashMap <Class <?>, Boolean >(64 );
8186
87+
88+ @ Override
8289 public int getOrder () {
8390 return LOWEST_PRECEDENCE ;
8491 }
@@ -127,12 +134,11 @@ else if (schedulers.size() == 1) {
127134 this .registrar .setScheduler (schedulers .values ().iterator ().next ());
128135 }
129136 else if (schedulers .size () >= 2 ){
130- throw new IllegalStateException (
131- "More than one TaskScheduler and/or ScheduledExecutorService " +
132- "exist within the context. Remove all but one of the beans; or " +
133- "implement the SchedulingConfigurer interface and call " +
134- "ScheduledTaskRegistrar#setScheduler explicitly within the " +
135- "configureTasks() callback. Found the following beans: " + schedulers .keySet ());
137+ throw new IllegalStateException ("More than one TaskScheduler and/or ScheduledExecutorService " +
138+ "exist within the context. Remove all but one of the beans; or implement the " +
139+ "SchedulingConfigurer interface and call ScheduledTaskRegistrar#setScheduler " +
140+ "explicitly within the configureTasks() callback. Found the following beans: " +
141+ schedulers .keySet ());
136142 }
137143 }
138144
@@ -145,126 +151,144 @@ public Object postProcessBeforeInitialization(Object bean, String beanName) {
145151 }
146152
147153 public Object postProcessAfterInitialization (final Object bean , String beanName ) {
148- final Class <?> targetClass = AopUtils .getTargetClass (bean );
149- ReflectionUtils .doWithMethods (targetClass , new MethodCallback () {
150- public void doWith (Method method ) throws IllegalArgumentException , IllegalAccessException {
151- Scheduled annotation = AnnotationUtils .getAnnotation (method , Scheduled .class );
152- if (annotation != null ) {
153- try {
154- Assert .isTrue (void .class .equals (method .getReturnType ()),
155- "Only void-returning methods may be annotated with @Scheduled" );
156- Assert .isTrue (method .getParameterTypes ().length == 0 ,
157- "Only no-arg methods may be annotated with @Scheduled" );
158- if (AopUtils .isJdkDynamicProxy (bean )) {
159- try {
160- // found a @Scheduled method on the target class for this JDK proxy -> is it
161- // also present on the proxy itself?
162- method = bean .getClass ().getMethod (method .getName (), method .getParameterTypes ());
163- }
164- catch (SecurityException ex ) {
165- ReflectionUtils .handleReflectionException (ex );
166- }
167- catch (NoSuchMethodException ex ) {
168- throw new IllegalStateException (String .format (
169- "@Scheduled method '%s' found on bean target class '%s', " +
170- "but not found in any interface(s) for bean JDK proxy. Either " +
171- "pull the method up to an interface or switch to subclass (CGLIB) " +
172- "proxies by setting proxy-target-class/proxyTargetClass " +
173- "attribute to 'true'" , method .getName (), targetClass .getSimpleName ()));
174- }
175- }
176- Runnable runnable = new ScheduledMethodRunnable (bean , method );
177- boolean processedSchedule = false ;
178- String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required" ;
179- // Determine initial delay
180- long initialDelay = annotation .initialDelay ();
181- String initialDelayString = annotation .initialDelayString ();
182- if (!"" .equals (initialDelayString )) {
183- Assert .isTrue (initialDelay < 0 , "Specify 'initialDelay' or 'initialDelayString', not both" );
184- if (embeddedValueResolver != null ) {
185- initialDelayString = embeddedValueResolver .resolveStringValue (initialDelayString );
186- }
187- try {
188- initialDelay = Integer .parseInt (initialDelayString );
189- }
190- catch (NumberFormatException ex ) {
191- throw new IllegalArgumentException (
192- "Invalid initialDelayString value \" " + initialDelayString + "\" - cannot parse into integer" );
193- }
194- }
195- // Check cron expression
196- String cron = annotation .cron ();
197- if (!"" .equals (cron )) {
198- Assert .isTrue (initialDelay == -1 , "'initialDelay' not supported for cron triggers" );
199- processedSchedule = true ;
200- if (embeddedValueResolver != null ) {
201- cron = embeddedValueResolver .resolveStringValue (cron );
202- }
203- registrar .addCronTask (new CronTask (runnable , cron ));
204- }
205- // At this point we don't need to differentiate between initial delay set or not anymore
206- if (initialDelay < 0 ) {
207- initialDelay = 0 ;
208- }
209- // Check fixed delay
210- long fixedDelay = annotation .fixedDelay ();
211- if (fixedDelay >= 0 ) {
212- Assert .isTrue (!processedSchedule , errorMessage );
213- processedSchedule = true ;
214- registrar .addFixedDelayTask (new IntervalTask (runnable , fixedDelay , initialDelay ));
215- }
216- String fixedDelayString = annotation .fixedDelayString ();
217- if (!"" .equals (fixedDelayString )) {
218- Assert .isTrue (!processedSchedule , errorMessage );
219- processedSchedule = true ;
220- if (embeddedValueResolver != null ) {
221- fixedDelayString = embeddedValueResolver .resolveStringValue (fixedDelayString );
222- }
223- try {
224- fixedDelay = Integer .parseInt (fixedDelayString );
225- }
226- catch (NumberFormatException ex ) {
227- throw new IllegalArgumentException (
228- "Invalid fixedDelayString value \" " + fixedDelayString + "\" - cannot parse into integer" );
229- }
230- registrar .addFixedDelayTask (new IntervalTask (runnable , fixedDelay , initialDelay ));
231- }
232- // Check fixed rate
233- long fixedRate = annotation .fixedRate ();
234- if (fixedRate >= 0 ) {
235- Assert .isTrue (!processedSchedule , errorMessage );
236- processedSchedule = true ;
237- registrar .addFixedRateTask (new IntervalTask (runnable , fixedRate , initialDelay ));
238- }
239- String fixedRateString = annotation .fixedRateString ();
240- if (!"" .equals (fixedRateString )) {
241- Assert .isTrue (!processedSchedule , errorMessage );
242- processedSchedule = true ;
243- if (embeddedValueResolver != null ) {
244- fixedRateString = embeddedValueResolver .resolveStringValue (fixedRateString );
245- }
246- try {
247- fixedRate = Integer .parseInt (fixedRateString );
248- }
249- catch (NumberFormatException ex ) {
250- throw new IllegalArgumentException (
251- "Invalid fixedRateString value \" " + fixedRateString + "\" - cannot parse into integer" );
252- }
253- registrar .addFixedRateTask (new IntervalTask (runnable , fixedRate , initialDelay ));
254- }
255- // Check whether we had any attribute set
256- Assert .isTrue (processedSchedule , errorMessage );
257- }
258- catch (IllegalArgumentException ex ) {
259- throw new IllegalStateException (
260- "Encountered invalid @Scheduled method '" + method .getName () + "': " + ex .getMessage ());
154+ if (!this .nonAnnotatedClasses .containsKey (bean .getClass ())) {
155+ final Set <Method > annotatedMethods = new LinkedHashSet <Method >(1 );
156+ Class <?> targetClass = AopUtils .getTargetClass (bean );
157+ ReflectionUtils .doWithMethods (targetClass , new MethodCallback () {
158+ public void doWith (Method method ) throws IllegalArgumentException , IllegalAccessException {
159+ Scheduled scheduled = AnnotationUtils .getAnnotation (method , Scheduled .class );
160+ if (scheduled != null ) {
161+ processScheduled (scheduled , method , bean );
162+ annotatedMethods .add (method );
261163 }
262164 }
165+ });
166+ if (annotatedMethods .isEmpty ()) {
167+ this .nonAnnotatedClasses .put (bean .getClass (), Boolean .TRUE );
263168 }
264- });
169+ }
265170 return bean ;
266171 }
267172
173+ private void processScheduled (Scheduled scheduled , Method method , Object bean ) {
174+ try {
175+ Assert .isTrue (void .class .equals (method .getReturnType ()),
176+ "Only void-returning methods may be annotated with @Scheduled" );
177+ Assert .isTrue (method .getParameterTypes ().length == 0 ,
178+ "Only no-arg methods may be annotated with @Scheduled" );
179+
180+ if (AopUtils .isJdkDynamicProxy (bean )) {
181+ try {
182+ // Found a @Scheduled method on the target class for this JDK proxy ->
183+ // is it also present on the proxy itself?
184+ method = bean .getClass ().getMethod (method .getName (), method .getParameterTypes ());
185+ }
186+ catch (SecurityException ex ) {
187+ ReflectionUtils .handleReflectionException (ex );
188+ }
189+ catch (NoSuchMethodException ex ) {
190+ throw new IllegalStateException (String .format (
191+ "@Scheduled method '%s' found on bean target class '%s', " +
192+ "but not found in any interface(s) for bean JDK proxy. Either " +
193+ "pull the method up to an interface or switch to subclass (CGLIB) " +
194+ "proxies by setting proxy-target-class/proxyTargetClass " +
195+ "attribute to 'true'" , method .getName (), method .getDeclaringClass ().getSimpleName ()));
196+ }
197+ }
198+
199+ Runnable runnable = new ScheduledMethodRunnable (bean , method );
200+ boolean processedSchedule = false ;
201+ String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required" ;
202+
203+ // Determine initial delay
204+ long initialDelay = scheduled .initialDelay ();
205+ String initialDelayString = scheduled .initialDelayString ();
206+ if (StringUtils .hasText (initialDelayString )) {
207+ Assert .isTrue (initialDelay < 0 , "Specify 'initialDelay' or 'initialDelayString', not both" );
208+ if (this .embeddedValueResolver != null ) {
209+ initialDelayString = this .embeddedValueResolver .resolveStringValue (initialDelayString );
210+ }
211+ try {
212+ initialDelay = Integer .parseInt (initialDelayString );
213+ }
214+ catch (NumberFormatException ex ) {
215+ throw new IllegalArgumentException (
216+ "Invalid initialDelayString value \" " + initialDelayString + "\" - cannot parse into integer" );
217+ }
218+ }
219+
220+ // Check cron expression
221+ String cron = scheduled .cron ();
222+ if (StringUtils .hasText (cron )) {
223+ Assert .isTrue (initialDelay == -1 , "'initialDelay' not supported for cron triggers" );
224+ processedSchedule = true ;
225+ if (this .embeddedValueResolver != null ) {
226+ cron = this .embeddedValueResolver .resolveStringValue (cron );
227+ }
228+ this .registrar .addCronTask (new CronTask (runnable , cron ));
229+ }
230+ // At this point we don't need to differentiate between initial delay set or not anymore
231+ if (initialDelay < 0 ) {
232+ initialDelay = 0 ;
233+ }
234+
235+ // Check fixed delay
236+ long fixedDelay = scheduled .fixedDelay ();
237+ if (fixedDelay >= 0 ) {
238+ Assert .isTrue (!processedSchedule , errorMessage );
239+ processedSchedule = true ;
240+ this .registrar .addFixedDelayTask (new IntervalTask (runnable , fixedDelay , initialDelay ));
241+ }
242+ String fixedDelayString = scheduled .fixedDelayString ();
243+ if (StringUtils .hasText (fixedDelayString )) {
244+ Assert .isTrue (!processedSchedule , errorMessage );
245+ processedSchedule = true ;
246+ if (this .embeddedValueResolver != null ) {
247+ fixedDelayString = this .embeddedValueResolver .resolveStringValue (fixedDelayString );
248+ }
249+ try {
250+ fixedDelay = Integer .parseInt (fixedDelayString );
251+ }
252+ catch (NumberFormatException ex ) {
253+ throw new IllegalArgumentException (
254+ "Invalid fixedDelayString value \" " + fixedDelayString + "\" - cannot parse into integer" );
255+ }
256+ this .registrar .addFixedDelayTask (new IntervalTask (runnable , fixedDelay , initialDelay ));
257+ }
258+
259+ // Check fixed rate
260+ long fixedRate = scheduled .fixedRate ();
261+ if (fixedRate >= 0 ) {
262+ Assert .isTrue (!processedSchedule , errorMessage );
263+ processedSchedule = true ;
264+ this .registrar .addFixedRateTask (new IntervalTask (runnable , fixedRate , initialDelay ));
265+ }
266+ String fixedRateString = scheduled .fixedRateString ();
267+ if (StringUtils .hasText (fixedRateString )) {
268+ Assert .isTrue (!processedSchedule , errorMessage );
269+ processedSchedule = true ;
270+ if (this .embeddedValueResolver != null ) {
271+ fixedRateString = this .embeddedValueResolver .resolveStringValue (fixedRateString );
272+ }
273+ try {
274+ fixedRate = Integer .parseInt (fixedRateString );
275+ }
276+ catch (NumberFormatException ex ) {
277+ throw new IllegalArgumentException (
278+ "Invalid fixedRateString value \" " + fixedRateString + "\" - cannot parse into integer" );
279+ }
280+ this .registrar .addFixedRateTask (new IntervalTask (runnable , fixedRate , initialDelay ));
281+ }
282+
283+ // Check whether we had any attribute set
284+ Assert .isTrue (processedSchedule , errorMessage );
285+ }
286+ catch (IllegalArgumentException ex ) {
287+ throw new IllegalStateException (
288+ "Encountered invalid @Scheduled method '" + method .getName () + "': " + ex .getMessage ());
289+ }
290+ }
291+
268292
269293 public void destroy () throws Exception {
270294 this .registrar .destroy ();
0 commit comments