springboot自定义配置MySQL_手写SpringBoot自动配置及自定义注解搭配Aop,实现升级版@Value()功能...

本文介绍了如何在SpringBoot中实现一个自定义注解,代替@Value从自定义配置中心(数据库)获取配置值。通过创建自动配置类、AOP切面以及自定义注解,实现在运行时动态修改对象属性,从而达到非侵入式的配置管理。
摘要由CSDN通过智能技术生成

背景

项目中为了统一管理项目的配置,比如接口地址,操作类别等信息,需要一个统一的配置管理中心,类似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 vals;

/**

* env type

*/

private String group;

/**

* get config

* @param key

* @return

*/

public String getValue(String key){

final List 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 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中我使用几种方式进行修改对象的属性。

20200719175507305309.png

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

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

20200719175507619775.png

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

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

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

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

20200719175507305309.png

首先上面已经明确,修改的应该是被代理对象的属性值。

我这里三种方法。第一种只修改一个新建对象的实例,很明显与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的实例,它的代理对象一定也被修改。我们调试后得出:

20200719175508124677.png

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

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

我们调用TestController 方法可以看到:

20200719175508607117.png

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值