diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/annotation/StepComponentBeanPostProcessor.java b/spring-batch-core/src/main/java/org/springframework/batch/core/annotation/StepComponentBeanPostProcessor.java new file mode 100644 index 000000000..aef2a27ab --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/annotation/StepComponentBeanPostProcessor.java @@ -0,0 +1,134 @@ +/** + * + */ +package org.springframework.batch.core.annotation; + +import java.lang.reflect.Method; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.support.DefaultIntroductionAdvisor; +import org.springframework.aop.support.DelegatingIntroductionInterceptor; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.StepExecutionListener; +import org.springframework.batch.core.step.AbstractStep; +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationUtils; + + +/** + * @author Lucas Ward + * + */ +public class StepComponentBeanPostProcessor implements BeanPostProcessor, Ordered { + + private AbstractStep step; + private String basePackage; + + public StepComponentBeanPostProcessor(AbstractStep step, String basePackage) { + this.step = step; + this.basePackage = basePackage; + } + + public Object postProcessAfterInitialization(Object bean, String beanName) + throws BeansException { + // if it implements the StepExecutionListener interface, we'll register + // it directly as such + + if (bean instanceof StepExecutionListener) { + step.registerStepExecutionListener((StepExecutionListener) bean); + } + + Class clazz = bean.getClass(); + if(bean instanceof FactoryBean){ + clazz = ((FactoryBean)bean).getObjectType(); + } + + // If the class isn't of the correct package, isn't annotated with the + // BatchComponent annotation, we won't put it in. + if (!clazz.getName().startsWith(basePackage) + || AnnotationUtils.findAnnotation(clazz, BatchComponent.class) == null) { + return bean; + } + + ProxyFactory proxyFactory = new ProxyFactory( + new Class[] { StepExecutionListener.class }); + + StepExecutionListenerMixin listenerMixin = new StepExecutionListenerMixin(bean); + + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if(AnnotationUtils.findAnnotation(method, BeforeStep.class) != null){ + listenerMixin.setBeforeStepMethod(method); + } + + if(AnnotationUtils.findAnnotation(method, AfterStep.class) != null){ + listenerMixin.setAfterStepMethod(method); + } + } + + proxyFactory.addAdvisor(new DefaultIntroductionAdvisor(listenerMixin, StepExecutionListener.class)); + StepExecutionListener listener = (StepExecutionListener)proxyFactory.getProxy(); + step.registerStepExecutionListener(listener); + + return null; + } + + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + + return bean; + } + + private class StepExecutionListenerMixin extends DelegatingIntroductionInterceptor implements StepExecutionListener{ + + Object bean; + Method afterStepMethod; + Method beforeStepMethod; + + public StepExecutionListenerMixin(Object bean) { + this.bean = bean; + } + + public void setBeforeStepMethod(Method beforeStepMethod) { + this.beforeStepMethod = beforeStepMethod; + } + + public void setAfterStepMethod(Method afterStepMethod) { + this.afterStepMethod = afterStepMethod; + } + + public ExitStatus afterStep(StepExecution stepExecution) { + if(afterStepMethod != null){ + invokeMethod(afterStepMethod, bean); + } + return null; + } + + public void beforeStep(StepExecution stepExecution) { + if(beforeStepMethod != null){ + invokeMethod(beforeStepMethod, bean); + } + } + } + + private Object invokeMethod(Method method, Object bean, Object... args){ + + try { + return method.invoke(bean, args); + } catch (Exception e) { + if(e instanceof RuntimeException){ + throw (RuntimeException)e; + } + throw new IllegalArgumentException("Failed to invoke method: [" + method + "]"); + } + } + + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/annotation/StepComponentBeanPostProcessorIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/annotation/StepComponentBeanPostProcessorIntegrationTests.java new file mode 100644 index 000000000..4f329487c --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/annotation/StepComponentBeanPostProcessorIntegrationTests.java @@ -0,0 +1,31 @@ +package org.springframework.batch.core.annotation; + + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.job.SimpleJob; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations={"component-scan-context.xml"}) +public class StepComponentBeanPostProcessorIntegrationTests { + + @Autowired + private SimpleJob job; + + @Autowired + private JobLauncher jobLauncher; + + @Test + public void testListener() throws Exception{ + + jobLauncher.run(job, new JobParameters()); + + // assertTrue(TestComponent.isAfterStepCalled()); + // assertTrue(TestComponent.isBeforeStepCalled()); + } +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/annotation/StepComponentBeanPostProcessorTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/annotation/StepComponentBeanPostProcessorTests.java new file mode 100644 index 000000000..446b9a6e3 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/annotation/StepComponentBeanPostProcessorTests.java @@ -0,0 +1,55 @@ +/** + * + */ +package org.springframework.batch.core.annotation; + +import static org.easymock.EasyMock.createMock; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.AbstractStep; +import org.springframework.batch.repeat.ExitStatus; + + +/** + * @author Lucas Ward + * + */ +public class StepComponentBeanPostProcessorTests { + + @BatchComponent + private class TestComponent { + + boolean afterStepCalled = false; + + @AfterStep + public void testMethod(){ + afterStepCalled = true; + } + } + + @Test + public void testNormalCase() throws Exception{ + AbstractStep step = new StubStep(); + step.setJobRepository(createMock(JobRepository.class)); + StepComponentBeanPostProcessor postProcessor = new StepComponentBeanPostProcessor(step, "org.springframework.batch.core.annotation"); + TestComponent testComponent = new TestComponent(); + postProcessor.postProcessAfterInitialization(testComponent, "testComponent"); + step.execute(new StepExecution("teststep", new JobExecution(11L))); + assertTrue(testComponent.afterStepCalled); + } + + private class StubStep extends AbstractStep{ + + @Override + protected ExitStatus doExecute(StepExecution stepExecution) + throws Exception { + // TODO Auto-generated method stub + return ExitStatus.FINISHED; + } + + } +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/annotation/TestComponent.java b/spring-batch-core/src/test/java/org/springframework/batch/core/annotation/TestComponent.java index e1d687654..9157550cb 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/annotation/TestComponent.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/annotation/TestComponent.java @@ -10,4 +10,25 @@ package org.springframework.batch.core.annotation; @BatchComponent public class TestComponent { + private static boolean beforeStepCalled = false; + + private static boolean afterStepCalled = false; + + public static boolean isBeforeStepCalled(){ + return beforeStepCalled; + } + + public static boolean isAfterStepCalled(){ + return afterStepCalled; + } + + @BeforeStep + public void beforeStep(){ + beforeStepCalled = true; + } + + @AfterStep + public void afterStep(){ + afterStepCalled = true; + } } diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/annotation/component-scan-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/annotation/component-scan-context.xml new file mode 100644 index 000000000..0366c8bc4 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/annotation/component-scan-context.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +