由JMS到BeanPostProcessor和SmartInitializingSingleton

起因

    最近手上项目从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/

发布了39 篇原创文章 · 获赞 9 · 访问量 1015

猜你喜欢

转载自blog.csdn.net/qq_30095631/article/details/103669089