1. 程式人生 > 實用技巧 >手寫SpringBoot自動配置及自定義註解搭配Aop,實現升級版@Value()功能

手寫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是[email protected],這其實代表著是物件本身而非代理物件。後面我們會看到,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 方法可以看到:



這裡我們可以看到,被代理的物件已經被成功修改,大功告成!