起因
最近手上项目从activemq切换阿里云的rocketmq,众所周知amq的listener属于JMS架构,spring项目中只需要在方法级别上加一个@Listener(destination=“xxx”)就好了,然后反手在看阿里云官方提供的样例代码,不免陷入了人生和社会的大思考。
为了看得更加明了,这是一段阿里云上给的rocketmq的消费者示例代码:
Properties properties = new Properties();
// 您在控制台创建的 Group ID
properties.put(PropertyKeyConst.GROUP_ID, "XXX");
// AccessKey 阿里云身份验证,在阿里云服务器管理控制台创建
properties.put(PropertyKeyConst.AccessKey, "XXX");
// SecretKey 阿里云身份验证,在阿里云服务器管理控制台创建
properties.put(PropertyKeyConst.SecretKey, "XXX");
// 设置 TCP 接入域名,进入控制台的实例管理页面的“获取接入点信息”区域查看
properties.put(PropertyKeyConst.NAMESRV_ADDR,
"XXX");
// 集群订阅方式 (默认)
// properties.put(PropertyKeyConst.MessageModel, PropertyValueConst.CLUSTERING);
// 广播订阅方式
// properties.put(PropertyKeyConst.MessageModel, PropertyValueConst.BROADCASTING);
Consumer consumer = ONSFactory.createConsumer(properties);
consumer.subscribe("TopicTestMQ", "TagA||TagB", new MessageListener() { //订阅多个 Tag
public Action consume(Message message, ConsumeContext context) {
System.out.println("Receive: " + message);
return Action.CommitMessage;
}
});
consumer.start();
System.out.println("Consumer Started");
可以看到每一个消费者都需要写这么一堆配置和启动,虽然量也不是很大,但相对于以前amq的时候只需要一个注解就解决的问题,这明显差了点档次,所以目标就是把代码改造成和JMS那样,当业务里需要写一个消费者的时候,只需要一个注解就能解决问题,而不用关心消费者是怎么配置怎么启动的,为此有了本篇文章。
错误尝试
一开始我想到了使用一个父类,让所有的监听者都继承父类,然后在构造方法中传入消费者的topic,groupId,url等等必要信息,然后在父类的构造方法中来创建对应的消费者,这确实是一个简单并且行之有效的方法。 但是还是没有达到一个注解解决问题的地步,要通过一个方法级注解解决这个问题,就意味着在spring启动过程中,需要找到这些有这个注解的对象已经方法,然后创建消费者,并且在消费者的回调函数中调用被注解的方法。一开始想到了ApplicationContextAware接口,可以拿到容器对象applicationContext:
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
application = applicationContext;
}
然后通过applicationContext来遍历所有对象中的所有方法,看看他们是否有我自定义的注解。这似乎是一个解决方案,但是明显很笨重,需要遍历Spring中所有的对象,如果说能够在spring创建对象的时候就能来判断是否有这个注解就好了。
正解
根据尝试中的痛点,思考良久,BeanPostProcessor横空出世。这个接口中有两个方法:postProcessBeforeInitialization和postProcessAfterInitialization。看名字就可以看出,当spring创建对象的前后分别会调用这两个方法,而我们就可以在对象创建之后,对这个对象进行方法级别的判断,找出那些有我的注解的对象以及方法了。
找到了这些方法和对象,那我们什么时候来创建消费者呢,可不可以直接在postProcessAfterInitialization方法中创建呢,经过思考后觉得是不安全的,因为对象创建过程中可能还有其他对象没有被创建,而此时消费者创建好了,一旦进行消费,就意味着会进入业务代码,而业务代码中会使用spring的哪一个对象是不可控的,如果这个对象还没有被创建,就意味着空指针(个人理解,或许是可以的)。
基于上面这一点个人理解,所以SmartInitializingSingleton被我相中了。这个接口中存在afterSingletonsInstantiated这个方法,他是当spring中所有的对象都创建完成后会被调用,此时来创建消费者或许是合适的。
以上是全部分析,下面给一下代码,鉴于不泄露公司源码的考虑,所以下面都是自己另外写的演示代码,没有业务逻辑,相对更简单。
这是注解类,最终业务类中使用只需要在方法上加上这么一个注解就完事了:
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnno {
String oneVal();
String twoVal();
}
这是消费者创建工厂,可以看到他继承了BeanPostProcessor和SmartInitializingSingleton,值得注意的是 postProcessBeforeInitialization和postProcessAfterInitialization两个方法是有返回值的,并且必须把方法的入参bean返回回去,如果返回null会有意想不到的问题(我就遇到返回null造成Tomcat无法启动)。我们在postProcessAfterInitialization方法中将有注解的方法和对象取出存储到List里面,等afterSingletonsInstantiated被运行的时候进行创建消费者。
public class Factory implements BeanPostProcessor, SmartInitializingSingleton {
private List<BeanContent> beanContents;
private String rootCfg;
public Factory(String rootCfg) {
this.rootCfg = rootCfg;
beanContents = new ArrayList<>();
}
String keyReg = "\\$\\{([^}]*)\\}";
@Override
public void afterSingletonsInstantiated() {
for (BeanContent beanContent : beanContents) {
try {
Method method = beanContent.getMethod();
Object bean = beanContent.getObj();
method.invoke(bean, new String(rootCfg + ":" + beanContent.getVal1() + beanContent.getVal2()));
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Method[] methods = ReflectionUtils.getAllDeclaredMethods(bean.getClass());
if (methods != null) {
for (Method method : methods) {
TestAnno testAnno = AnnotationUtils.findAnnotation(method, TestAnno.class);
if (testAnno != null) {
String val1 = getKeyPath(testAnno.oneVal());
String val2 = getKeyPath(testAnno.twoVal());
beanContents.add(new BeanContent(bean, method, val1, val2));
}
}
}
return bean;
}
public String getKeyPath(String str) {
Pattern regex = Pattern.compile(keyReg);
Matcher matcher = regex.matcher(str);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
}
BeanContent类是用来存储对象中的一些信息的,只是一个基本的javaBean,就不贴代码了。
最后是一个MyConfig,将Factory对象进行手动注入,你可能问为什么不通过注解进行自动注入呢,原因是需要注入一些参数,如果不需要注入参数,完全可以通过注解自动注入。
@Configuration
public class MyConfig {
@Bean
public Factory getProcesser(){
Factory factory = new Factory("ROOT_CONFIG");
return factory;
}
}
最后是使用,只需要在test方法上加上一个TestAnno注解即可,是不是很简单。
@Component
public class TestBean {
@TestAnno(oneVal = "${123}",twoVal = "${789}")
public void test(String msg){
System.out.println("只行到此处:"+msg);
}
}
转载自本人自建小站:https://www.yyf256.top/blog/