zoukankan      html  css  js  c++  java
  • 手写SpringBoot自动配置及自定义注解搭配Aop,实现升级版@Value()功能

    背景

    项目中为了统一管理项目的配置,比如接口地址,操作类别等信息,需要一个统一的配置管理中心,类似nacos。
    我根据项目的需求写了一套分布式配置中心,测试无误后,改为单体应用并耦合到项目中。项目中使用配置文件多是取配置文件(applicatoion.yml)的值,使用@Value获取,为了秉持非侵入性的原则,我决定写一套自定义注解,以实现最少的代码量实现业务需求。

    思路

    需要实现类似springboot @Value注解获取配置文件对应key的值的功能。但区别在于 我是从自己写的自动配置中获取,原理就是数据库中查询所有的配置信息,并放入一个对象applicationConfigContext,同时创建一个bean交给spring托管,同时写了个aop,为被注解的属性赋入applicationConfigContext的对应的值。
    换句话说,自定义的这个注解为类赋值的时间线大概是

     spring bean初始化 —->  第三方插件初始化 --> 我写的自动配置初始化   ---- 用户调用某个方法,触发aop机制,我通过反射动态改变了触发aop的对象的bean的属性,将值赋值给他。
    

    难点

    本项目的难点在于如何修改对象的值。看似简单,其实里面的文章很多。

    自动配置代码

    配置映射数据库pojo

    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import java.util.Date;
    
    /**
     * @Describtion config bean
     * @Author yonyong
     * @Date 2020/7/13 15:43
     * @Version 1.0.0
     **/
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder(toBuilder = true)
    public class TblConfig {
        private Integer id;
    
        /**
         * 配置名称
         */
        private String keyName;
    
        /**
         * 默认配置值
         */
        private String keyValue;
    
        /**
         * 分类
         */
        private String keyGroup;
    
        /**
         * 备注
         */
        private String description;
    
        /**
         * 创建时间
         */
        private Date insertTime;
    
        /**
         * 更新时间
         */
        private Date updateTime;
    
        /**
         * 创建人
         */
        private String creator;
    
        private Integer start;
    
        private Integer rows;
    
        /**
         * 是否是系统自带
         */
        private String type;
    
        /**
         * 修改人
         */
        private String modifier;
    }
    

    创建用于防止配置信息的对象容器

    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import java.util.List;
    import java.util.stream.Collectors;
    
    /**
     * @Describtion config container
     * @Author yonyong
     * @Date 2020/7/13 15:40
     * @Version 1.0.0
     **/
    @Data
    @Builder(toBuilder = true)
    @AllArgsConstructor
    @NoArgsConstructor
    public class ConfigContext {
    
        /**
         * config key-val map
         */
        private List<TblConfig> vals;
    
        /**
         * env type
         */
        private String group;
    
        /**
         * get config
         * @param key
         * @return
         */
        public String getValue(String key){
            final List<TblConfig> collect = vals.stream()
                    .filter(tblConfig -> tblConfig.getKeyName().equals(key))
                    .collect(Collectors.toList());
            if (null == collect || collect.size() == 0)
                return null;
            return collect.get(0).getKeyValue();
        }
    }
    

    创建配置,查询出数据库里配置并创建一个容器bean

    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.beans.factory.config.ConfigurableBeanFactory;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Scope;
    
    import javax.annotation.Resource;
    import java.util.List;
    
    /**
     * @Describtion manual auto inject bean
     * @Author yonyong
     * @Date 2020/7/13 15:55
     * @Version 1.0.0
     **/
    @Configuration
    @ConditionalOnClass(ConfigContext.class)
    public class ConfigContextAutoConfig {
    
        @Value("${config.center.group:DEFAULT_ENV}")
        private String group;
    
        @Resource
        private TblConfigcenterMapper tblConfigcenterMapper;
    
        @Bean(name = "applicationConfigContext")
        @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
        @ConditionalOnMissingBean(ConfigContext.class)
        public ConfigContext myConfigContext() {
            ConfigContext configContext = ConfigContext.builder().build();
            //set group
            if (StringUtils.isNotBlank(group))
                group = "DEFAULT_ENV";
            //set vals
            TblConfig tblConfig = TblConfig.builder().keyGroup(group).build();
            final List<TblConfig> tblConfigs = tblConfigcenterMapper.selectByExample(tblConfig);
            configContext = configContext.toBuilder()
                    .vals(tblConfigs)
                    .group(group)
                    .build();
            return configContext;
        }
    }
    

    AOP相关代码

    创建自定义注解

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @Author yonyong
     * @Description //配置
     * @Date 2020/7/17 11:20
     * @Param 
     * @return 
     **/
    @Target({ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyConfig {
        /**
         * 如果此value为空,修改值为获取当前group,不为空正常获取配置文件中指定key的val
         * @return
         */
        String value() default "";
        Class<?> clazz() default MyConfig.class;
    }
    

    创建aop业务功能

    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.StringUtils;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Field;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    import java.util.Date;
    
    /**
     * @Describtion config service aop
     * @Author yonyong
     * @Date 2020/7/17 11:21
     * @Version 1.0.0
     **/
    @Aspect
    @Component
    @Slf4j
    public class SystemConfigAop {
    
        @Autowired
        ConfigContext applicationConfigContext;
    
        @Autowired
        MySpringContext mySpringContext;
    
        @Pointcut("@annotation(com.ai.api.config.configcenter.aop.MyConfig)")
        public void pointcut(){}
    
        @Before("pointcut()")
        public void before(JoinPoint joinPoint){
            final MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            MyConfig myConfig = method.getAnnotation(MyConfig.class);
            Class<?> clazz = myConfig.clazz();
            final Field[] declaredFields = clazz.getDeclaredFields();
            Object bean = mySpringContext.getBean(clazz);
            for (Field declaredField : declaredFields) {
                final MyConfig annotation = declaredField.getAnnotation(MyConfig.class);
                if (null != annotation && StringUtils.isNotBlank(annotation.value())){
                    log.info(annotation.value());
                    String val = getVal(annotation.value());
                    try {
    //                    setFieldData(declaredField,clazz.newInstance(),val);
    //                    setFieldData(declaredField,bean,val);
                        buildMethod(clazz,bean,declaredField,val);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
    //        mySpringContext.refresh(bean.getClass());
        }
    
        private void setFieldData(Field field, Object bean, String data) throws Exception {
            // 注意这里要设置权限为true
            field.setAccessible(true);
            Class<?> type = field.getType();
            if (type.equals(String.class)) {
                field.set(bean, data);
            } else if (type.equals(Integer.class)) {
                field.set(bean, Integer.valueOf(data));
            } else if (type.equals(Long.class)) {
                field.set(bean, Long.valueOf(data));
            } else if (type.equals(Double.class)) {
                field.set(bean, Double.valueOf(data));
            } else if (type.equals(Short.class)) {
                field.set(bean, Short.valueOf(data));
            } else if (type.equals(Byte.class)) {
                field.set(bean, Byte.valueOf(data));
            } else if (type.equals(Boolean.class)) {
                field.set(bean, Boolean.valueOf(data));
            } else if (type.equals(Date.class)) {
                field.set(bean, new Date(Long.valueOf(data)));
            }
        }
    
        private String getVal(String key){
            if (StringUtils.isNotBlank(key)){
                return applicationConfigContext.getValue(key);
            }else {
                return applicationConfigContext.getGroup();
            }
        }
    
        private void buildMethod(Class<?> clz ,Object obj,Field field,String propertiedValue) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
            // 获取属性的名字
            String name = field.getName();
            // 将属性的首字符大写, 构造get,set方法
            name = name.substring(0, 1).toUpperCase() + name.substring(1);
            // 获取属性的类型
            String type = field.getGenericType().toString();
            // 如果type是类类型,则前面包含"class ",后面跟类名
            // String 类型
            if (type.equals("class java.lang.String")) {
                Method m = clz.getMethod("set" + name, String.class);
                // invoke方法传递实例对象,因为要对实例处理,而不是类
                m.invoke(obj, propertiedValue);
            }
            // int Integer类型
            if (type.equals("class java.lang.Integer")) {
                Method m = clz.getMethod("set" + name, Integer.class);
                m.invoke(obj, Integer.parseInt(propertiedValue));
            }
            if (type.equals("int")) {
                Method m = clz.getMethod("set" + name, int.class);
                m.invoke(obj, (int) Integer.parseInt(propertiedValue));
            }
            // boolean Boolean类型
            if (type.equals("class java.lang.Boolean")) {
                Method m = clz.getMethod("set" + name, Boolean.class);
                if (propertiedValue.equalsIgnoreCase("true")) {
                    m.invoke(obj, true);
                }
                if (propertiedValue.equalsIgnoreCase("false")) {
                    m.invoke(obj, true);
                }
            }
            if (type.equals("boolean")) {
                Method m = clz.getMethod("set" + name, boolean.class);
                if (propertiedValue.equalsIgnoreCase("true")) {
                    m.invoke(obj, true);
                }
                if (propertiedValue.equalsIgnoreCase("false")) {
                    m.invoke(obj, true);
                }
            }
            // long Long 数据类型
            if (type.equals("class java.lang.Long")) {
                Method m = clz.getMethod("set" + name, Long.class);
                m.invoke(obj, Long.parseLong(propertiedValue));
            }
            if (type.equals("long")) {
                Method m = clz.getMethod("set" + name, long.class);
                m.invoke(obj, Long.parseLong(propertiedValue));
            }
            // 时间数据类型
            if (type.equals("class java.util.Date")) {
                Method m = clz.getMethod("set" + name, java.util.Date.class);
                m.invoke(obj, DataConverter.convert(propertiedValue));
            }
        }
    }
    

    使用方式demo类

    @RestController
    @RequestMapping("/version")
    @Api(tags = "版本")
    @ApiSort(value = 0)
    @Data
    public class VersionController {
        
        @MyConfig("opcl.url")
        public String url = "1";
        
        @GetMapping(value="/test", produces = "application/json;charset=utf-8")
        @MyConfig(clazz = VersionController.class)
        public Object test(){
            return url;
        }
    
    }
    

    这里如果想在VersionController 注入配置url,首先需要在配置url上添加注解MyConfig,value为配置在容器中的key;其次需要在使用url的方法test上添加注解MyConfig,并将当前class传入,当调用此方法,便会触发aop机制,更新url的值

    开发过程遇到的问题

    简述

    在aop中我使用几种方式进行修改对象的属性。

    最终是是第三种证实修改成功。首先spring的bean都是采用动态代理的方式产生。而默认的都是采用单例模式。所以我们需要搞清楚:

    versioncontroller方法中拿取url这个属性时,拿取者是谁?

    versioncontroller方法中拿取url这个属性时,拿取者是谁,是VersionController还是spring进行cglib动态代理产生的bean(以下简称bean)?

    这里可以看到Versioncontroller的方法执行时,这里的this是Versioncontroller@9250,这其实代表着是对象本身而非代理对象。后面我们会看到,springbean其实是代理对象代理了被代理对象,执行了其(Versioncontroller)方法。

    我们的目的是修改什么?是修改VersionController还是这个bean?

    我们讲到,springbean其实是代理对象代理了被代理对象,执行了其(Versioncontroller)方法。那么我们修改的理所应该是被代理对象的属性值。

    当进行反射赋值的时候,我们修改的是VersionController这个类还是bean?

    首先上面已经明确,修改的应该是被代理对象的属性值。
    我这里三种方法。第一种只修改一个新建对象的实例,很明显与springbean理念相悖,不可能实现我们的需求,所以只谈后两种。

    先看第二种是通过工具类获取bean,然后通过反射为对应的属性赋值。
    这里写一个testController便于验证。

    package com.ai.api.controller;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/test")
    public class TestController {
        @Autowired
        VersionController versionController;
    
        @GetMapping("/1")
        public Object getUrl(){
            System.out.println(versionController.getUrl());
            System.out.println(versionController.url);
            return versionController.getUrl();
        }
    }
    
    

    这里我们是直接为bean的属性赋值。我们先调用VersionController中的test方法,让其先走一遍Aop。因为springbean如果没有配置,默认的都是单例模式,所以说如果修改成功,那么testController中,注入的VersionController,因为是同一个VersionController的实例,它的代理对象一定也被修改。我们调试后得出:

    我们可以看到,我们确实修改掉了bean的值,但被代理对象的url仍然是1。并没有实现我们想要的效果。

    第三种,通过获取这个bean,通过这个代理bean的set方法,间接修改被代理对象VersionController的属性值。我们先调用VersionController中的test方法,让其先走一遍Aop,因为springbean如果没有配置,默认的都是单例模式。如果修改成功,那么testController中,注入的VersionController,因为是同一个VersionController的实例,它的代理对象一定也被修改了。
    我们调用TestController 方法可以看到:

    这里我们可以看到,被代理的对象已经被成功修改,大功告成!

  • 相关阅读:
    星球居民突破 1800 人!
    测试数据管理
    解决InnoDB: Table mysql/innodb_index_stats has length mismatch in the column name table_name. Please run mysql_upgrade
    Warning: file_get_contents(): open_basedir restriction in effect. File(/proc/uptime) is not within the allowed path(s)解决方法
    Java终止线程的三种方式
    线程中断interrupt
    Linux 开启防火墙 避免非干系人误操作的处理
    Oracle12c 快速启动命令设置
    Docker 运行 Redis Rabbitmq seata-server ftp 的简单办法
    mysql8 CentOS7 简要安装说明
  • 原文地址:https://www.cnblogs.com/yonyong/p/13339583.html
Copyright © 2011-2022 走看看