大家好,我是小义,又来写技术文章了。开年本想着顺顺利利大展一番拳脚,可没想到,第一个 Bug 就给我来了个 “下马威”,而罪魁祸首,居然就是 Java 里那个让人又爱又恨的反射机制!今天就来和大家唠唠我这 “惨痛” 的经历,也顺便一起把这反射机制再好好捋捋。
下面结合代码,简单复现一下事情经过。小义想实现一个策略模式,需要把实现了某个父类的所有子类都放到一个Map中。首先我们自定义了一个注解,用于标识子类,即Map的key值。
//自定义注解,用于标识客户端
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ClientType {
String value() default "";
}
接着定义父类和子类,这里假设业务场景是要实现不同类型客户端的服务请求,有android安卓、harmony鸿蒙、apple苹果等等,分别继承BaseService父类,并且通过自定义注解@ClientType区分。
//服务实现父类
public class BaseService {
//...
}
//安卓端服务类
@Service
@ClientType(value = "android")
public class AndroidService extends BaseService {
//...
}
//苹果端服务类
@Service
@ClientType(value = "apple")
public class AppleService extends BaseService {
//...
}
//鸿蒙端服务类
@Service
@ClientType(value = "harmony")
public class HarmonyService extends BaseService {
@Transactional(rollbackFor = Exception.class)
public String doService() {
//...
return "success";
}
}
接下来是重中之重,利用Java反射遍历出所有子类,并转换为Map的形式缓存起来。 具体代码如下。
其中第三点注释的地方就是bug所在。细心的小伙伴可能发现了,HarmonyService和其他的继承了BeanService的Bean不同,它在处理业务代码时用到了事务,所以需要在方法上加上@Transactional事务注解。不加还没事,原本不加的时候,利用注释掉的那一行代码的getClass().getAnnotation()方法也能实现小义想要的效果,但加上后麻烦就来了。
在 Spring 中,给方法加上@Transactional注解后,通过 Spring 内部的 AOP 机制、对注解的解析以及代理对象的创建等一系列操作,就能使得该方法被 Spring 进行事务增强,从而方便地实现事务管理功能,保障了数据操作在事务控制下的一致性和完整性。也就是说Spring创建出了HarmonyService的代理对象,而这个对象在类上面是没有ClientType这个注解的。
//服务管理器
@Component
public class ServiceManager implements ApplicationContextAware {
private ApplicationContext applicationContext;
private Map<String, BaseService> clientServiceMap = new HashMap<>();
@PostConstruct
public void init() {
//1、利用Spring应用上下文获取所有继承了BaseService的Bean实例,并将这些实例以BaseService类型的集合形式进行获取
Collection<BaseService> values = applicationContext.getBeansOfType(BaseService.class).values();
//2、遍历集合,取出带自定义注解的Bean
for (BaseService handler : values) {
//3、注释行在判断HarmonyService时,无法取到ClientType注解
//ClientType annotation = (ClientType) AnnotationUtils.findAnnotation(printHandler.getClass(), ClientType.class);
ClientType annotation = (ClientType) handler.getClass().getAnnotation(ClientType.class);
if (null != annotation && null != annotation.value()) {
//4、以ClientType的value为key,Bean示例为value保存至Map
clientServiceMap.put(annotation.value(), handler);
}
}
}
/**
* 获取服务
*/
public BaseService getClientService(String clientType){
return clientServiceMap.get(clientType);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
解决的方法也很简单,换成用AnnotationUtils工具类去获取注解就好了。 AnnotationUtils#findAnnotation方法会先在该实现类查找注解,如果没有找到,则递归地在父类中查找注解,直到找到注解或到达Object类为止。
//org.springframework.core.annotation.AnnotationUtils#findAnnotation核心代码
public static <A extends Annotation> A findAnnotation(Class<?> clazz, @Nullable Class<A> annotationType) {
//...
if (AnnotationFilter.PLAIN.matches(annotationType) ||
AnnotationsScanner.hasPlainJavaAnnotationsOnly(clazz)) {
A annotation = clazz.getDeclaredAnnotation(annotationType);
if (annotation != null) {
return annotation;
}
Class<?> superclass = clazz.getSuperclass();
if (superclass == null || superclass == Object.class) {
return null;
}
return findAnnotation(superclass, annotationType);
}
//...
}
好了,bug解决完了,虽然只是替换简单的一行代码,但一不小心可能就有个大坑在等着你。我是小义,希望大家一起学习。