代理对象注入

定制配置的获取中,我们可以通过对一个配置的Bean进行代理,使他能够自动地切换客户来获取配置,对用户屏蔽具体的实现细节。我们要实现对一个有@Config注解的Bean对象进行增强,然后将代理Bean注入Spring容器中。配置Bean示例代码如下。

具体使用方法参考:https://kb.cvte.com/pages/viewpage.action?pageId=320163817。

@Data
@Config(prefix = "email-server", separator = ".")
public class EmailServer {
    private String host = "email-smtp.eu-west-1.amazonaws.com";
    private Integer port = 465;
    private String from = "noreply@bytello.com";
    private String nickName = "Bytello";
    /**
     * email-server.account.username
     */
    @ConfigKey("account.username")
    private String username = "bytello";
    /**
     * email-server.account.password
     */
    @ConfigKey("account.password")
    private String password = "BDrm6MyBBr/hj";
}

方法一 Spring AOP

采用了AOP拦截基础包的所有方法,并对这个方法所在的类进行判断,判断这个类有没有@Config注解,有就进行增强处理。

拦截器 MethodInterceptor

public class ConfigAnnotationInterceptor implements MethodInterceptor {

    private CustomizerConfig customizerConfig;

    public ConfigAnnotationInterceptor(){
        this.customizerConfig = SpringUtil.getBean(CustomizerConfig.class);
    }

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        //判断是否有Config注解,没有就直接方法执行结果(默认值)
        Object defaultValue = methodInvocation.proceed();
        Config annotation = methodInvocation.getMethod().getDeclaringClass().getAnnotation(Config.class);
        if (annotation == null) {
            return defaultValue;
        }
        //获取方法名,截取getXxx的属性值xxx
        String key = methodInvocation.getMethod().getName().substring(3);
        String lowercaseKey = StringUtils.uncapitalize(key);

        //查出值并做类型转化
        Object value = customizerConfig.getProperty(lowercaseKey, defaultValue);
        Object res = new ObjectMapper().convertValue(value, methodInvocation.getMethod().getReturnType());
        return res;
    }
}

配置类

配置 Advisor 并注入容器

public static final String EXPRESSION = "execution(* %s..*.get*(..))";

@Bean
public DefaultPointcutAdvisor defaultPointcutAdvisor() {
    String execution = String.format(EXPRESSION, customizerConfiguration.getBasePackage());
    ConfigAnnotationInterceptor interceptor = new ConfigAnnotationInterceptor();
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression(execution);

    // 配置增强类advisor
    DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
    advisor.setPointcut(pointcut);
    advisor.setAdvice(interceptor);
    return advisor;
}

方法二 动态代理对象注入

​ 其实AOP就是动态代理,但是不够灵活,EXPRESSION 表达式将包里所有的类都进行了拦截代理,也就是符合表达式的所有方法都会先走到增强方法里判断有没有注解。我们可以精准的找到有注解的对象,对该对象进行代理并注入到 Spring 容器中。

CGLib动态代理工厂

public class ProxyBeanFactory {
    public static <T> T getProxy(Class<T> clazz) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(new ProxyInterceptor());
        T proxy = (T) enhancer.create();
        return proxy;
    }
}

ConfigFactoryBean

往 Spring 容器注入代理对象

public class ConfigFactoryBean<T> implements FactoryBean<T> {

    private Class<T> clazz;

    public ConfigFactoryBean(Class<T> clazz) {
        this.clazz = clazz;
    }

    @Override
    public T getObject() {
        T proxy = ProxyBeanFactory.getProxy(clazz);
        return proxy;
    }

    @Override
    public Class<?> getObjectType() {
        return clazz;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

ConfigBeanRegister

将 ConfigFactoryBean 在实例化之前注入到容器,这时 FactoryBean 才会生效,并且必须在属性注入之前生效,否则@Autowire 会找不到对应的Bean,导致注入失败。

public class ConfigBeanRegister implements BeanDefinitionRegistryPostProcessor {

    private String basePackage = "com";

    public ConfigBeanRegister(String basePackage) {
        this.basePackage = basePackage;
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {

        Reflections f = new Reflections(basePackage);
        Set<Class<?>> set = f.getTypesAnnotatedWith(Config.class);
        for (Class<?> clazz : set) {
            try {
              //带参 beanDefinition 创建
                GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
                beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(clazz);
                beanDefinition.setBeanClass(ConfigFactoryBean.class);
                registry.registerBeanDefinition(clazz.getName(), beanDefinition);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    }

}

问题

1、代理 一个类的 get 方法,会不会导致一些异常情况?

​ 唯一的异常情况就是通过反射的时候获取实例的 field 成员变量的值和定义为静态公共变量直接访问,这两种情况都不会走到代理,但是这是少数情况,我们可以在使用文档做一些规范约束,给用户一些提示。

​ 以下几种情况都可以正常返回配置。因为底层都是调用他的 get 方法来实现。

​ 1)BeanUtils 的类字段映射

​ 2)直接打印类 System.out.println(x x x)

​ 3)将类作为 Controller 的返回值

​ 4)作为Dubbo远程调用参数