手写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还是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 方法可以看到:
这里我们可以看到,被代理的对象已经被成功修改,大功告成!
手写SpringBoot自动配置及自定义注解搭配Aop,实现升级版@Value()功能的更多相关文章
- 助力SpringBoot自动配置的条件注解ConditionalOnXXX分析--SpringBoot源码(三)
注:该源码分析对应SpringBoot版本为2.1.0.RELEASE 1 前言 本篇接 如何分析SpringBoot源码模块及结构?--SpringBoot源码(二) 上一篇分析了SpringBoo ...
- 这样讲 SpringBoot 自动配置原理,你应该能明白了吧
https://juejin.im/post/5ce5effb6fb9a07f0b039a14 前言 小伙伴们是否想起曾经被 SSM 整合支配的恐惧?相信很多小伙伴都是有过这样的经历的,一大堆配置问题 ...
- SpringBoot实战之SpringBoot自动配置原理
SpringBoot 自动配置主要通过 @EnableAutoConfiguration, @Conditional, @EnableConfigurationProperties 或者 @Confi ...
- springboot自动配置源码解析
springboot版本:2.1.6.RELEASE SpringBoot 自动配置主要通过 @EnableAutoConfiguration, @Conditional, @EnableConfig ...
- SpringBoot 自动配置:Spring Data JPA
前言 不知道从啥时候开始项目上就一直用MyBatis,其实我个人更新JPA些,因为JPA看起来OO的思想更强烈些,所以这才最近把JPA拿出来再看一看,使用起来也很简单,除了定义Entity实体外,声明 ...
- springboot自动配置原理以及手动实现配置类
springboot自动配置原理以及手动实现配置类 1.原理 spring有一个思想是"约定大于配置". 配置类自动配置可以帮助开发人员更加专注于业务逻辑开发,springboot ...
- 案例解析:springboot自动配置未生效问题定位(条件断点)
Spring Boot在为开发人员提供更高层次的封装,进而提高开发效率的同时,也为出现问题时如何进行定位带来了一定复杂性与难度.但Spring Boot同时又提供了一些诊断工具来辅助开发与分析,如sp ...
- 源码学习系列之SpringBoot自动配置(篇一)
源码学习系列之SpringBoot自动配置源码学习(篇一) ok,本博客尝试跟一下Springboot的自动配置源码,做一下笔记记录,自动配置是Springboot的一个很关键的特性,也容易被忽略的属 ...
- Springboot 自动配置浅析
Introduction 我们知道,SpringBoot之所以强大,就是因为他提供了各种默认的配置,可以让我们在集成各个组件的时候从各种各样的配置文件中解放出来. 拿一个最普通的 web 项目举例.我 ...
随机推荐
- 1.二进制部署kubernetes
目录 kubernetes的五个组件 master节点的三个组件 kube-apiserver kube-controller-manager kube-scheduler node节点的两个组件 k ...
- 【Key】 Windows 95 到 Windows10所有KEY 包括OEM系统
部分可能不准确,请见谅,数据源自Baidu,由noogai00整理,数据为Microsoft.所有 Windows 95 02398-OEM-0030022-5886409297-OEM-002128 ...
- 分享2个近期遇到的MySQL数据库的BUG案例
近一个月处理历史数据问题时,居然连续遇到了2个MySQL BUG,分享给大家一下,也欢迎指正是否有问题. BUG1: 数据库版本: MySQL5.7.25 - 28 操作系统: Centos 7.7 ...
- GitHub 热点速览 Vol.25:距离优雅编程你差个它
作者:HelloGitHub-小鱼干 摘要:如何优雅地夸一个程序员呢?vscode-rainbow-fart 作为一个彩虹屁的项目,深得程序员心,能在你编程时疯狂称赞你的除了你自己,还有它.除了鼓励之 ...
- JavaWeb网上图书商城完整项目--day02-20.修改密码各层实现
1.我们来看看后台操作的业务流程 每一层都按照上面的步骤来进行实现: 这里我们要使用commUtils.toBean把表单提交的参数封装成User对象,必须保证User对象中的字段和表单提交的字段的名 ...
- mybatis面试入门
第一步创建一个java project 导入mybatis需要的jar包,创建与数据库一一对应的javabean对象 第二步:创建mybatis的配置文件 sqlMapconfig.xml 第三步:创 ...
- 一文告诉你Linux如何配置KVM虚拟化--安装篇
KVM全称"Kernel-based Virtual Machine",即基于内核的虚拟机,在linux内启用kvm需要硬件,内核和软件(qemu)支持,这篇文章教你如何配置并安装 ...
- DOM-BOM-EVENT(3)
3.Node常用属性 childNodes 获取所有子节点 <div id="wrap"> <div>1111</div> <div> ...
- 洛谷 P6136 【【模板】普通平衡树(数据加强版)】
爱死替罪羊树了 这种暴力的数据结构爱死了.什么?!你还不知道替罪羊树?那就看看这篇博客这篇博客吧.替罪羊树就是当不平衡时,拍扁重建,然后就平衡了.想切这道题,要先把普通平衡树那道题做了(这篇博客讲了的 ...
- 分享一个与jQuery相关的TypeError: $ is not a function问题解决过程
最近碰到一个比较奇葩的问题,估计很多人也遇到过,就是jQuery可能会遇到的‘$ is not a function’,不过我碰到的这个问题比较怪异,解决该问题也颇费了一番周折,现在给大家分享一下. ...