BATCH-1118:
*Updated JobListenerFactoryBean so that listener methods specified via annotations or the metaDataMap are checked eagerly for correct signatures *Extracted common processing of Job/StepListenerFactoryBean classes into new superclass AbstractListenerFactoryBean *Created AbstractListenerMetaData interface for Job/StepListenerMetaData enums *Fixed JobExecutionListenerParser which had a bug in parsing listener method attributes *Corrected job listener attributes in xsd to use before/after-job-method *Added unit tests for job listener annotations and metaDataMap *Fixed typo in StepListenerFactoryBeanTests
This commit is contained in:
@@ -83,14 +83,15 @@ public class JobExecutionListenerParser {
|
||||
}
|
||||
|
||||
ManagedMap metaDataMap = new ManagedMap();
|
||||
String beforeMethod = listenerElement.getAttribute("before-method");
|
||||
if (StringUtils.hasText(beforeMethod)) {
|
||||
metaDataMap.put("beforeMethod", beforeMethod);
|
||||
}
|
||||
|
||||
String afterMethod = listenerElement.getAttribute("after-method");
|
||||
if (StringUtils.hasText(beforeMethod)) {
|
||||
metaDataMap.put("afterMethod", afterMethod);
|
||||
String[] methodNameAttributes = new String[] {
|
||||
"before-job-method",
|
||||
"after-job-method"
|
||||
};
|
||||
for (String metaDataPropertyName : methodNameAttributes) {
|
||||
String listenerMethod = listenerElement.getAttribute(metaDataPropertyName);
|
||||
if(StringUtils.hasText(listenerMethod)){
|
||||
metaDataMap.put(metaDataPropertyName, listenerMethod);
|
||||
}
|
||||
}
|
||||
listenerBuilder.addPropertyValue("metaDataMap", metaDataMap);
|
||||
AbstractBeanDefinition beanDef = listenerBuilder.getBeanDefinition();
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Copyright 2002-2008 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.batch.core.listener;
|
||||
|
||||
import static org.springframework.batch.support.MethodInvokerUtils.getParamTypesString;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.aop.framework.Advised;
|
||||
import org.springframework.batch.support.MethodInvoker;
|
||||
import org.springframework.batch.support.MethodInvokerUtils;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* {@link FactoryBean} implementation that builds a listener based on the
|
||||
* various lifecycle methods or annotations that are provided. There are three
|
||||
* possible ways of having a method called as part of a listener lifecycle:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Interface implementation: By implementing any of the subclasses of a
|
||||
* listener interface, methods on said interface will be called
|
||||
* <li>Annotations: Annotating a method will result in registration.
|
||||
* <li>String name of the method to be called, which is tied to an
|
||||
* {@link AbstractListenerMetaData} in the metaDatMap.
|
||||
* </ul>
|
||||
*
|
||||
* It should be noted that methods obtained by name or annotation that don't
|
||||
* match the listener method signatures to which they belong will cause errors.
|
||||
* However, it is acceptable to have no parameters at all. If the same method is
|
||||
* marked in more than one way. (i.e. the method name is given and it is
|
||||
* annotated) the method will only be called once. However, if the same class
|
||||
* has multiple methods tied to a particular listener, each method will be
|
||||
* called. Also note that the same annotations cannot be applied to two separate
|
||||
* methods in a single class.
|
||||
*
|
||||
* @author Lucas Ward
|
||||
* @author Dan Garrette
|
||||
* @since 2.0
|
||||
* @see StepListenerMetaData
|
||||
*/
|
||||
public abstract class AbstractListenerFactoryBean implements FactoryBean, InitializingBean {
|
||||
|
||||
private static Log logger = LogFactory.getLog(StepListenerFactoryBean.class);
|
||||
|
||||
private Object delegate;
|
||||
|
||||
private Map<String, String> metaDataMap;
|
||||
|
||||
public Object getObject() {
|
||||
|
||||
if (metaDataMap == null) {
|
||||
metaDataMap = new HashMap<String, String>();
|
||||
}
|
||||
// Because all annotations and interfaces should be checked for, make
|
||||
// sure that each meta data
|
||||
// entry is represented.
|
||||
for (AbstractListenerMetaData metaData : this.getMetaDataValues()) {
|
||||
if (!metaDataMap.containsKey(metaData.getPropertyName())) {
|
||||
// put null so that the annotation and interface is checked
|
||||
metaDataMap.put(metaData.getPropertyName(), null);
|
||||
}
|
||||
}
|
||||
|
||||
return this.doGetObject(delegate, metaDataMap);
|
||||
}
|
||||
|
||||
protected abstract Object doGetObject(Object delegate, Map<String, String> metaDataMap);
|
||||
|
||||
protected abstract AbstractListenerMetaData[] getMetaDataValues();
|
||||
|
||||
/**
|
||||
* Create a MethodInvoker from the delegate based on the annotationType.
|
||||
* Ensure that the annotated method has a valid set of parameters.
|
||||
*
|
||||
* @param metaData
|
||||
* @return a MethodInvoker
|
||||
*/
|
||||
protected MethodInvoker getMethodInvokerByAnnotation(final AbstractListenerMetaData metaData) {
|
||||
final Class<? extends Annotation> annotationType = metaData.getAnnotation();
|
||||
MethodInvoker mi = MethodInvokerUtils.getMethodInvokerByAnnotation(annotationType, delegate);
|
||||
if (mi != null) {
|
||||
ReflectionUtils.doWithMethods(delegate.getClass(), new ReflectionUtils.MethodCallback() {
|
||||
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
|
||||
Annotation annotation = AnnotationUtils.findAnnotation(method, annotationType);
|
||||
if (annotation != null) {
|
||||
Class<?>[] paramTypes = method.getParameterTypes();
|
||||
if (paramTypes.length > 0) {
|
||||
Class<?>[] expectedParamTypes = metaData.getParamTypes();
|
||||
|
||||
String errorMsg = "The method [" + method.getName() + "] on target class ["
|
||||
+ delegate.getClass().getSimpleName() + "] is incompatable with the signature ["
|
||||
+ getParamTypesString(expectedParamTypes) + "] expected for the annotation ["
|
||||
+ metaData.getAnnotation().getSimpleName() + "].";
|
||||
|
||||
Assert.isTrue(paramTypes.length == expectedParamTypes.length, errorMsg);
|
||||
for (int i = 0; i < paramTypes.length; i++) {
|
||||
Assert.isTrue(paramTypes[i].equals(expectedParamTypes[i]), errorMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return mi;
|
||||
}
|
||||
|
||||
protected MethodInvoker getMethodInvokerByName(String methodName, Object candidate, Class<?>... params) {
|
||||
if (methodName != null) {
|
||||
return MethodInvokerUtils.createMethodInvokerByName(candidate, methodName, false, params);
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSingleton() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setDelegate(Object delegate) {
|
||||
if (delegate instanceof Advised) {
|
||||
try {
|
||||
setDelegate(((Advised) delegate).getTargetSource().getTarget());
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Cannot generate listener for proxy with no target", e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
}
|
||||
|
||||
public void setMetaDataMap(Map<String, String> metaDataMap) {
|
||||
this.metaDataMap = metaDataMap;
|
||||
}
|
||||
|
||||
/*
|
||||
* Extension of HashSet that ignores nulls, rather than putting them into
|
||||
* the set.
|
||||
*/
|
||||
protected static class NullIgnoringSet<E> extends HashSet<E> {
|
||||
|
||||
@Override
|
||||
public boolean add(E e) {
|
||||
if (e == null) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
return super.add(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
Assert.notNull(delegate, "Delegate must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to check whether the given object is or can be made
|
||||
* into a listener.
|
||||
*
|
||||
* @param delegate the object to check
|
||||
* @return true if the delegate is an instance of any of the listener
|
||||
* interface, or contains the marker annotations
|
||||
*/
|
||||
public static boolean isListener(Object delegate, Class<?> listenerType, AbstractListenerMetaData[] metaDataValues) {
|
||||
if (listenerType.isInstance(delegate)) {
|
||||
return true;
|
||||
}
|
||||
if (delegate instanceof Advised) {
|
||||
try {
|
||||
return isListener(((Advised) delegate).getTargetSource().getTarget(), listenerType, metaDataValues);
|
||||
} catch (Exception e) {
|
||||
logger.debug("Error obtaining target for Proxy. Assume not a listener.", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (AbstractListenerMetaData metaData : metaDataValues) {
|
||||
if (MethodInvokerUtils.getMethodInvokerByAnnotation(metaData.getAnnotation(), delegate) != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2002-2008 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.batch.core.listener;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
|
||||
/**
|
||||
* A common interface for ListenerMetaData enumerations.
|
||||
*
|
||||
* @author Dan Garrette
|
||||
* @since 2.0
|
||||
* @see JobListenerMetaData
|
||||
* @see StepListenerMetaData
|
||||
*/
|
||||
public interface AbstractListenerMetaData {
|
||||
|
||||
public String getMethodName();
|
||||
|
||||
public Class<? extends Annotation> getAnnotation();
|
||||
|
||||
public String getPropertyName();
|
||||
|
||||
public Class<?>[] getParamTypes();
|
||||
|
||||
}
|
||||
@@ -15,11 +15,9 @@
|
||||
*/
|
||||
package org.springframework.batch.core.listener;
|
||||
|
||||
import static org.springframework.batch.support.MethodInvokerUtils.getMethodInvokerByAnnotation;
|
||||
import static org.springframework.batch.support.MethodInvokerUtils.getMethodInvokerForInterface;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Map.Entry;
|
||||
@@ -29,76 +27,33 @@ import org.springframework.aop.support.DefaultPointcutAdvisor;
|
||||
import org.springframework.batch.core.JobExecution;
|
||||
import org.springframework.batch.core.JobExecutionListener;
|
||||
import org.springframework.batch.support.MethodInvoker;
|
||||
import org.springframework.batch.support.MethodInvokerUtils;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.batch.core.listener.MethodInvokerMethodInterceptor;
|
||||
|
||||
/**
|
||||
* {@link FactoryBean} implementation that builds a {@link JobExecutionListener}
|
||||
* based on the various lifecycle methods or annotations that are provided.
|
||||
* There are three possible ways of having a method called as part of a
|
||||
* {@link JobExecutionListener} lifecyle:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Interface implementation: By implementing JobExecutionListener, methods
|
||||
* on said interface will be called.
|
||||
* <li>Annotations: Annotating a method will result in registration.
|
||||
* <li>String name of the method to be called, which is tied to
|
||||
* {@link JobListenerMetaData} in the metaDatMap.
|
||||
* </ul>
|
||||
*
|
||||
* It should be noted that methods obtained by name or annotation that don't
|
||||
* match the listener method signatures to which they belong, will cause errors.
|
||||
* However, it is acceptable to have no parameters at all. If the same method is
|
||||
* marked in more than one way. (i.e. the method name is given and it's
|
||||
* annotated) the method will only be called once. However, if the same class
|
||||
* has multiple methods tied to a particular listener, each method will be
|
||||
* called.
|
||||
* This {@link AbstractListenerFactoryBean} implementation is used to create a
|
||||
* {@link JobExecutionListener}.
|
||||
*
|
||||
* @author Lucas Ward
|
||||
* @author Dan Garrette
|
||||
* @since 2.0
|
||||
* @see AbstractListenerFactoryBean
|
||||
* @see JobListenerMetaData
|
||||
*/
|
||||
public class JobListenerFactoryBean implements FactoryBean, InitializingBean {
|
||||
public class JobListenerFactoryBean extends AbstractListenerFactoryBean {
|
||||
|
||||
private Object delegate;
|
||||
|
||||
private Map<String, String> metaDataMap;
|
||||
|
||||
public void setDelegate(Object delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public void setMetaDataMap(Map<String, String> metaDataMap) {
|
||||
this.metaDataMap = metaDataMap;
|
||||
}
|
||||
|
||||
public Object getObject() {
|
||||
Map<String, Set<MethodInvoker>> invokerMap = new HashMap<String, Set<MethodInvoker>>();
|
||||
if (metaDataMap == null) {
|
||||
metaDataMap = new HashMap<String, String>();
|
||||
}
|
||||
// Because all annotations and interfaces should be checked for, make
|
||||
// sure that each meta data
|
||||
// entry is represented.
|
||||
for (JobListenerMetaData metaData : JobListenerMetaData.values()) {
|
||||
if (!metaDataMap.containsKey(metaData.getPropertyName())) {
|
||||
// put null so that the annotation and interface is checked
|
||||
metaDataMap.put(metaData.getPropertyName(), null);
|
||||
}
|
||||
}
|
||||
public Object doGetObject(Object delegate, Map<String, String> metaDataMap) {
|
||||
|
||||
// For every entry in the map, try and find a method by interface, name,
|
||||
// or annotation. If the same
|
||||
Map<String, Set<MethodInvoker>> invokerMap = new HashMap<String, Set<MethodInvoker>>();
|
||||
for (Entry<String, String> entry : metaDataMap.entrySet()) {
|
||||
JobListenerMetaData metaData = JobListenerMetaData.fromPropertyName(entry.getKey());
|
||||
Set<MethodInvoker> invokers = new NullIgnoringSet<MethodInvoker>();
|
||||
invokers.add(getMethodInvokerByName(entry.getValue(), delegate, JobExecution.class));
|
||||
invokers.add(getMethodInvokerByName(entry.getValue(), delegate, metaData.getParamTypes()));
|
||||
invokers.add(getMethodInvokerForInterface(JobExecutionListener.class, metaData.getMethodName(), delegate,
|
||||
JobExecution.class));
|
||||
invokers.add(getMethodInvokerByAnnotation(metaData.getAnnotation(), delegate));
|
||||
invokers.add(getMethodInvokerByAnnotation(metaData));
|
||||
if (!invokers.isEmpty()) {
|
||||
invokerMap.put(metaData.getMethodName(), invokers);
|
||||
}
|
||||
@@ -108,7 +63,7 @@ public class JobListenerFactoryBean implements FactoryBean, InitializingBean {
|
||||
// be called
|
||||
ProxyFactory proxyFactory = new ProxyFactory();
|
||||
proxyFactory.setTarget(delegate);
|
||||
|
||||
|
||||
boolean ordered = false;
|
||||
if (delegate instanceof Ordered) {
|
||||
ordered = true;
|
||||
@@ -120,13 +75,8 @@ public class JobListenerFactoryBean implements FactoryBean, InitializingBean {
|
||||
return proxyFactory.getProxy();
|
||||
}
|
||||
|
||||
private MethodInvoker getMethodInvokerByName(String methodName, Object candidate, Class<?>... params) {
|
||||
if (methodName != null) {
|
||||
return MethodInvokerUtils.createMethodInvokerByName(candidate, methodName, false, params);
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
protected AbstractListenerMetaData[] getMetaDataValues() {
|
||||
return JobListenerMetaData.values();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -134,17 +84,10 @@ public class JobListenerFactoryBean implements FactoryBean, InitializingBean {
|
||||
return JobExecutionListener.class;
|
||||
}
|
||||
|
||||
public boolean isSingleton() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
Assert.notNull(delegate, "Delegate listener must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to wrap any object and expose the appropriate
|
||||
* {@link JobExecutionListener} interfaces.
|
||||
*
|
||||
* @param delegate a delegate object
|
||||
* @return a JobListener instance constructed from the delegate
|
||||
*/
|
||||
@@ -157,38 +100,13 @@ public class JobListenerFactoryBean implements FactoryBean, InitializingBean {
|
||||
/**
|
||||
* Convenience method to check whether the given object is or can be made
|
||||
* into a {@link JobExecutionListener}.
|
||||
*
|
||||
* @param delegate the object to check
|
||||
* @return true if the delegate is an instance of
|
||||
* {@link JobExecutionListener}, or contains the marker annotations
|
||||
* {@link JobExecutionListener}, or contains the marker annotations
|
||||
*/
|
||||
public static boolean isListener(Object delegate) {
|
||||
if (delegate instanceof JobExecutionListener) {
|
||||
return true;
|
||||
}
|
||||
for (JobListenerMetaData metaData : JobListenerMetaData.values()) {
|
||||
Set<MethodInvoker> invokers = new NullIgnoringSet<MethodInvoker>();
|
||||
invokers.add(getMethodInvokerByAnnotation(metaData.getAnnotation(), delegate));
|
||||
if (!invokers.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Extension of HashSet that ignores nulls, rather than putting them into
|
||||
* the set.
|
||||
*/
|
||||
private static class NullIgnoringSet<E> extends HashSet<E> {
|
||||
|
||||
@Override
|
||||
public boolean add(E e) {
|
||||
if (e == null) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
return super.add(e);
|
||||
}
|
||||
};
|
||||
return AbstractListenerFactoryBean.isListener(delegate, JobExecutionListener.class, JobListenerMetaData
|
||||
.values());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import java.lang.annotation.Annotation;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.batch.core.JobExecution;
|
||||
import org.springframework.batch.core.JobExecutionListener;
|
||||
import org.springframework.batch.core.annotation.AfterJob;
|
||||
import org.springframework.batch.core.annotation.BeforeJob;
|
||||
@@ -31,7 +32,7 @@ import org.springframework.batch.core.annotation.BeforeJob;
|
||||
* @since 2.0
|
||||
* @see JobListenerFactoryBean
|
||||
*/
|
||||
public enum JobListenerMetaData {
|
||||
public enum JobListenerMetaData implements AbstractListenerMetaData {
|
||||
|
||||
BEFORE_JOB("beforeJob", "before-job-method", BeforeJob.class),
|
||||
AFTER_JOB("afterJob", "after-job-method", AfterJob.class);
|
||||
@@ -63,11 +64,14 @@ public enum JobListenerMetaData {
|
||||
return annotation;
|
||||
}
|
||||
|
||||
|
||||
public String getPropertyName() {
|
||||
return propertyName;
|
||||
}
|
||||
|
||||
public Class<?>[] getParamTypes() {
|
||||
return new Class<?>[]{ JobExecution.class };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the relevant meta data for the provided property name.
|
||||
*
|
||||
|
||||
@@ -16,92 +16,45 @@
|
||||
package org.springframework.batch.core.listener;
|
||||
|
||||
import static org.springframework.batch.support.MethodInvokerUtils.getMethodInvokerForInterface;
|
||||
import static org.springframework.batch.support.MethodInvokerUtils.getParamTypesString;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.aop.framework.Advised;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.aop.support.DefaultPointcutAdvisor;
|
||||
import org.springframework.batch.core.StepListener;
|
||||
import org.springframework.batch.support.MethodInvoker;
|
||||
import org.springframework.batch.support.MethodInvokerUtils;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* {@link FactoryBean} implementation that builds a {@link StepListener} based
|
||||
* on the various lifecycle methods or annotations that are provided. There are
|
||||
* three possible ways of having a method called as part of a
|
||||
* {@link StepListener} lifecycle:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Interface implementation: By implementing any of the subclasses of
|
||||
* StepListener, methods on said interface will be called
|
||||
* <li>Annotations: Annotating a method will result in registration.
|
||||
* <li>String name of the method to be called, which is tied to
|
||||
* {@link StepListenerMetaData} in the metaDatMap.
|
||||
* </ul>
|
||||
*
|
||||
* It should be noted that methods obtained by name or annotation that don't
|
||||
* match the StepListener method signatures to which they belong, will cause
|
||||
* errors. However, it is acceptable to have no parameters at all. If the same
|
||||
* method is marked in more than one way. (i.e. the method name is given and
|
||||
* it's annotated) the method will only be called once. However, if the same
|
||||
* class has multiple methods tied to a particular listener, each method will be
|
||||
* called.
|
||||
* This {@link AbstractListenerFactoryBean} implementation is used to create a
|
||||
* {@link StepListener}.
|
||||
*
|
||||
* @author Lucas Ward
|
||||
* @author Dan Garrette
|
||||
* @since 2.0
|
||||
* @see AbstractListenerFactoryBean
|
||||
* @see StepListenerMetaData
|
||||
*/
|
||||
public class StepListenerFactoryBean implements FactoryBean, InitializingBean {
|
||||
public class StepListenerFactoryBean extends AbstractListenerFactoryBean {
|
||||
|
||||
private static Log logger = LogFactory.getLog(StepListenerFactoryBean.class);
|
||||
|
||||
private Object delegate;
|
||||
|
||||
private Map<String, String> metaDataMap;
|
||||
|
||||
public Object getObject() {
|
||||
|
||||
Map<String, Set<MethodInvoker>> invokerMap = new HashMap<String, Set<MethodInvoker>>();
|
||||
if (metaDataMap == null) {
|
||||
metaDataMap = new HashMap<String, String>();
|
||||
}
|
||||
// Because all annotations and interfaces should be checked for, make
|
||||
// sure that each meta data
|
||||
// entry is represented.
|
||||
for (StepListenerMetaData metaData : StepListenerMetaData.values()) {
|
||||
if (!metaDataMap.containsKey(metaData.getPropertyName())) {
|
||||
// put null so that the annotation and interface is checked
|
||||
metaDataMap.put(metaData.getPropertyName(), null);
|
||||
}
|
||||
}
|
||||
public Object doGetObject(Object delegate, Map<String, String> metaDataMap) {
|
||||
|
||||
Set<Class<?>> listenerInterfaces = new HashSet<Class<?>>();
|
||||
|
||||
// For every entry in the map, try and find a method by interface, name,
|
||||
// or annotation. If the same
|
||||
Map<String, Set<MethodInvoker>> invokerMap = new HashMap<String, Set<MethodInvoker>>();
|
||||
for (Entry<String, String> entry : metaDataMap.entrySet()) {
|
||||
final StepListenerMetaData metaData = StepListenerMetaData.fromPropertyName(entry.getKey());
|
||||
Set<MethodInvoker> invokers = new NullIgnoringSet<MethodInvoker>();
|
||||
invokers.add(getMethodInvokerByName(entry.getValue(), delegate, metaData.getParamTypes()));
|
||||
invokers.add(getMethodInvokerForInterface(metaData.getListenerInterface(), metaData.getMethodName(),
|
||||
delegate, metaData.getParamTypes()));
|
||||
invokers.add(getMethodInvokerByAnnotation(metaData, metaData.getAnnotation()));
|
||||
invokers.add(getMethodInvokerByAnnotation(metaData));
|
||||
if (!invokers.isEmpty()) {
|
||||
invokerMap.put(metaData.getMethodName(), invokers);
|
||||
listenerInterfaces.add(metaData.getListenerInterface());
|
||||
@@ -127,50 +80,8 @@ public class StepListenerFactoryBean implements FactoryBean, InitializingBean {
|
||||
return proxyFactory.getProxy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a MethodInvoker from the delegate based on the annotationType.
|
||||
* Ensure that the annotated method has a valid set of parameters.
|
||||
*
|
||||
* @param metaData
|
||||
* @param annotationType
|
||||
* @return
|
||||
*/
|
||||
private MethodInvoker getMethodInvokerByAnnotation(final StepListenerMetaData metaData,
|
||||
final Class<? extends Annotation> annotationType) {
|
||||
MethodInvoker mi = MethodInvokerUtils.getMethodInvokerByAnnotation(annotationType, delegate);
|
||||
if (mi != null) {
|
||||
ReflectionUtils.doWithMethods(delegate.getClass(), new ReflectionUtils.MethodCallback() {
|
||||
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
|
||||
Annotation annotation = AnnotationUtils.findAnnotation(method, annotationType);
|
||||
if (annotation != null) {
|
||||
Class<?>[] paramTypes = method.getParameterTypes();
|
||||
if (paramTypes.length > 0) {
|
||||
Class<?>[] expectedParamTypes = metaData.getParamTypes();
|
||||
|
||||
String errorMsg = "The method [" + method.getName() + "] on target class ["
|
||||
+ delegate.getClass().getSimpleName() + "] is incompatable with the signature ["
|
||||
+ getParamTypesString(expectedParamTypes) + "] expected for the annotation ["
|
||||
+ metaData.getAnnotation().getSimpleName() + "].";
|
||||
|
||||
Assert.isTrue(paramTypes.length == expectedParamTypes.length, errorMsg);
|
||||
for (int i = 0; i < paramTypes.length; i++) {
|
||||
Assert.isTrue(paramTypes[i].equals(expectedParamTypes[i]), errorMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return mi;
|
||||
}
|
||||
|
||||
private MethodInvoker getMethodInvokerByName(String methodName, Object candidate, Class<?>... params) {
|
||||
if (methodName != null) {
|
||||
return MethodInvokerUtils.createMethodInvokerByName(candidate, methodName, false, params);
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
protected AbstractListenerMetaData[] getMetaDataValues() {
|
||||
return StepListenerMetaData.values();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -178,28 +89,6 @@ public class StepListenerFactoryBean implements FactoryBean, InitializingBean {
|
||||
return StepListener.class;
|
||||
}
|
||||
|
||||
public boolean isSingleton() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setDelegate(Object delegate) {
|
||||
if (delegate instanceof Advised) {
|
||||
try {
|
||||
setDelegate(((Advised) delegate).getTargetSource().getTarget());
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalStateException("Cannot generate listener for proxy with no target", e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
}
|
||||
|
||||
public void setMetaDataMap(Map<String, String> metaDataMap) {
|
||||
this.metaDataMap = metaDataMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to wrap any object and expose the appropriate
|
||||
* {@link StepListener} interfaces.
|
||||
@@ -222,44 +111,6 @@ public class StepListenerFactoryBean implements FactoryBean, InitializingBean {
|
||||
* {@link StepListener} interfaces, or contains the marker annotations
|
||||
*/
|
||||
public static boolean isListener(Object delegate) {
|
||||
if (delegate instanceof StepListener) {
|
||||
return true;
|
||||
}
|
||||
if (delegate instanceof Advised) {
|
||||
try {
|
||||
return isListener(((Advised) delegate).getTargetSource().getTarget());
|
||||
}
|
||||
catch (Exception e) {
|
||||
logger.debug("Error obtaining target for Proxy. Assume not a listener.", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (StepListenerMetaData metaData : StepListenerMetaData.values()) {
|
||||
if (MethodInvokerUtils.getMethodInvokerByAnnotation(metaData.getAnnotation(), delegate) != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Extension of HashSet that ignores nulls, rather than putting them into
|
||||
* the set.
|
||||
*/
|
||||
private static class NullIgnoringSet<E> extends HashSet<E> {
|
||||
|
||||
@Override
|
||||
public boolean add(E e) {
|
||||
if (e == null) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
return super.add(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
Assert.notNull(delegate, "Delegate must not be null");
|
||||
return AbstractListenerFactoryBean.isListener(delegate, StepListener.class, StepListenerMetaData.values());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ import org.springframework.batch.core.annotation.OnWriteError;
|
||||
* @since 2.0
|
||||
* @see StepListenerFactoryBean
|
||||
*/
|
||||
public enum StepListenerMetaData {
|
||||
public enum StepListenerMetaData implements AbstractListenerMetaData {
|
||||
|
||||
BEFORE_STEP("beforeStep", "before-step-method", BeforeStep.class, StepExecutionListener.class, StepExecution.class),
|
||||
AFTER_STEP("afterStep", "after-step-method", AfterStep.class, StepExecutionListener.class, StepExecution.class),
|
||||
|
||||
@@ -229,7 +229,8 @@
|
||||
<xsd:attribute name="ref" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
A reference to a JobExecutionListener or a POJO (with before-method / after-method).
|
||||
A reference to a JobExecutionListener or a POJO
|
||||
(with before-job-method / after-job-method).
|
||||
</xsd:documentation>
|
||||
<xsd:appinfo>
|
||||
<tool:annotation kind="ref"/>
|
||||
@@ -248,8 +249,8 @@
|
||||
</xsd:appinfo>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="before-method" type="xsd:string" />
|
||||
<xsd:attribute name="after-method" type="xsd:string" />
|
||||
<xsd:attribute name="before-job-method" type="xsd:string" />
|
||||
<xsd:attribute name="after-job-method" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:sequence>
|
||||
|
||||
@@ -17,8 +17,11 @@ package org.springframework.batch.core.listener;
|
||||
|
||||
import static junit.framework.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.springframework.batch.core.listener.JobListenerMetaData.AFTER_JOB;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Before;
|
||||
@@ -28,6 +31,7 @@ import org.springframework.batch.core.JobExecution;
|
||||
import org.springframework.batch.core.JobExecutionListener;
|
||||
import org.springframework.batch.core.annotation.AfterJob;
|
||||
import org.springframework.batch.core.annotation.BeforeJob;
|
||||
import org.springframework.batch.core.configuration.xml.AbstractTestComponent;
|
||||
import org.springframework.core.Ordered;
|
||||
|
||||
/**
|
||||
@@ -138,6 +142,100 @@ public class JobListenerFactoryBeanTests {
|
||||
assertEquals(listener1, listener2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptySignatureAnnotation() {
|
||||
AbstractTestComponent delegate = new AbstractTestComponent() {
|
||||
@SuppressWarnings("unused")
|
||||
@AfterJob
|
||||
public void aMethod() {
|
||||
executed = true;
|
||||
}
|
||||
};
|
||||
factoryBean.setDelegate(delegate);
|
||||
JobExecutionListener listener = (JobExecutionListener) factoryBean.getObject();
|
||||
listener.afterJob(new JobExecution(1L));
|
||||
assertTrue(delegate.isExecuted());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRightSignatureAnnotation() {
|
||||
AbstractTestComponent delegate = new AbstractTestComponent() {
|
||||
@SuppressWarnings("unused")
|
||||
@AfterJob
|
||||
public void aMethod(JobExecution jobExecution) {
|
||||
executed = true;
|
||||
assertEquals(new Long(25), jobExecution.getId());
|
||||
}
|
||||
};
|
||||
factoryBean.setDelegate(delegate);
|
||||
JobExecutionListener listener = (JobExecutionListener) factoryBean.getObject();
|
||||
listener.afterJob(new JobExecution(25L));
|
||||
assertTrue(delegate.isExecuted());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testWrongSignatureAnnotation() {
|
||||
AbstractTestComponent delegate = new AbstractTestComponent() {
|
||||
@SuppressWarnings("unused")
|
||||
@AfterJob
|
||||
public void aMethod(Integer item) {
|
||||
executed = true;
|
||||
}
|
||||
};
|
||||
factoryBean.setDelegate(delegate);
|
||||
factoryBean.getObject();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptySignatureNamedMethod() {
|
||||
AbstractTestComponent delegate = new AbstractTestComponent() {
|
||||
@SuppressWarnings("unused")
|
||||
public void aMethod() {
|
||||
executed = true;
|
||||
}
|
||||
};
|
||||
factoryBean.setDelegate(delegate);
|
||||
Map<String, String> metaDataMap = new HashMap<String, String>();
|
||||
metaDataMap.put(AFTER_JOB.getPropertyName(), "aMethod");
|
||||
factoryBean.setMetaDataMap(metaDataMap);
|
||||
JobExecutionListener listener = (JobExecutionListener) factoryBean.getObject();
|
||||
listener.afterJob(new JobExecution(1L));
|
||||
assertTrue(delegate.isExecuted());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRightSignatureNamedMethod() {
|
||||
AbstractTestComponent delegate = new AbstractTestComponent() {
|
||||
@SuppressWarnings("unused")
|
||||
public void aMethod(JobExecution jobExecution) {
|
||||
executed = true;
|
||||
assertEquals(new Long(25), jobExecution.getId());
|
||||
}
|
||||
};
|
||||
factoryBean.setDelegate(delegate);
|
||||
Map<String, String> metaDataMap = new HashMap<String, String>();
|
||||
metaDataMap.put(AFTER_JOB.getPropertyName(), "aMethod");
|
||||
factoryBean.setMetaDataMap(metaDataMap);
|
||||
JobExecutionListener listener = (JobExecutionListener) factoryBean.getObject();
|
||||
listener.afterJob(new JobExecution(25L));
|
||||
assertTrue(delegate.isExecuted());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testWrongSignatureNamedMethod() {
|
||||
AbstractTestComponent delegate = new AbstractTestComponent() {
|
||||
@SuppressWarnings("unused")
|
||||
public void aMethod(Integer item) {
|
||||
executed = true;
|
||||
}
|
||||
};
|
||||
factoryBean.setDelegate(delegate);
|
||||
Map<String, String> metaDataMap = new HashMap<String, String>();
|
||||
metaDataMap.put(AFTER_JOB.getPropertyName(), "aMethod");
|
||||
factoryBean.setMetaDataMap(metaDataMap);
|
||||
factoryBean.getObject();
|
||||
}
|
||||
|
||||
private class JobListenerWithInterface implements JobExecutionListener {
|
||||
|
||||
boolean beforeJobCalled = false;
|
||||
|
||||
@@ -311,7 +311,6 @@ public class StepListenerFactoryBeanTests {
|
||||
public void testEmptySignatureNamedMethod() {
|
||||
AbstractTestComponent delegate = new AbstractTestComponent() {
|
||||
@SuppressWarnings("unused")
|
||||
@AfterWrite
|
||||
public void aMethod() {
|
||||
executed = true;
|
||||
}
|
||||
@@ -330,7 +329,6 @@ public class StepListenerFactoryBeanTests {
|
||||
public void testRightSignatureNamedMethod() {
|
||||
AbstractTestComponent delegate = new AbstractTestComponent() {
|
||||
@SuppressWarnings("unused")
|
||||
@AfterWrite
|
||||
public void aMethod(List<String> items) {
|
||||
executed = true;
|
||||
assertEquals("foo", items.get(0));
|
||||
@@ -351,7 +349,6 @@ public class StepListenerFactoryBeanTests {
|
||||
public void testWrongSignatureNamedMethod() {
|
||||
AbstractTestComponent delegate = new AbstractTestComponent() {
|
||||
@SuppressWarnings("unused")
|
||||
@AfterWrite
|
||||
public void aMethod(Integer item) {
|
||||
executed = true;
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<job id="job" incrementer="testIncrementer" job-repository="jobRepository">
|
||||
<step id="s1" ref="step1"/>
|
||||
<listeners>
|
||||
<listener after-method="afterJob" ref="testListener"/>
|
||||
<listener after-method="afterJob" class="org.springframework.batch.core.configuration.xml.TestJobListener"/>
|
||||
<listener after-job-method="afterJob" ref="testListener"/>
|
||||
<listener after-job-method="afterJob" class="org.springframework.batch.core.configuration.xml.TestJobListener"/>
|
||||
<listener class="org.springframework.batch.core.configuration.xml.JobExecutionListenerParserTests$TestComponent"/>
|
||||
</listeners>
|
||||
</job>
|
||||
|
||||
Reference in New Issue
Block a user