上级文章:
快速Mybatis动态数据源(基于java类):https://blog.youkuaiyun.com/qq_28033719/article/details/103609120
XXL-JOB设置前规则后规则:https://blog.youkuaiyun.com/qq_28033719/article/details/103701084
问题:
spring - 4.1.2 版本,首先 spring 的 AOP 是可以开启 cglib 的代理,这意味着,AOP 原理是 cglib 的处理(具体了解 cglib 的代理写法)。
如果是没有内部调用,spring 的 AOP 会这样运行:
Spring会进入这个 CglibAopProxy 里面,调用 DynamicAdvisedInterceptor(spring自己封装的 cglib 类),然后这个类会最后这样子:
其实这里 target 就是 spring 里面存放的一个, spring 实例,并不是 cglib 给我们生成的那个 新建的 代理父类,那么一连串的调用链之后,进行调用的就是 spring 的 实例:
cglib 有个方法,是使用 mathodproxy.invokesuper(proxy, args) 这个方法如果进行内部调用的话,也可以被拦截!!
那么spring 的 cglib 会使用自己的一个封装的代理类进行处理,但是该代理类最后调用的是 springbean(spring自己的bean类)方法进行 invoke ,如果这样子,我之前做的 AOP 切面,会在内部调用的时候失效,如下图:
如果我进行一个内部调用,getPhoneOne 调用 getPhoneList 的时候,我的 AOP 是切入在 DBSource 这里的,但是,因为 Spring 的 AOP 机制,所以会导致我调用 getPhoneOne 的时候,spring 是使用 method.invoke(springbean, args) 调用 spring 的实例化类进行方法的调用, 所以,不会经过 @DBSource 的切面。
最终抛错,如下图,具体为什么会抛出错,是因为我上级文章写了动态数据源注解,会从注解获取指定相应的数据源:
那这样子,我的动态数据源注解就会失效了,如果要封装一次 Service ,然后其他service调用这个封装的 service ,也是不适合已有业务的,并且有大量的代码侵入。
解决方法:
那么就直接使用 cglib ,CGLIB 里面有个方法其实是 methodProxy.invokeSuper(o, objects) 这样子进行方法调用是可以解决内部调用的,原理是:
上图我用反射偷看了 cglib 的生成类,里面字段都是代理我给他的 SuperClass 的所有方法的代理类,所以 cglib 可以解决内部调用问题。
然后使用 BeanPostProcessor 这个是 spring bean 的初始化监控,这个 processor 其实是 spring 容器 初始化类 的 processor调用链中的一环:
MySpringBeanProcessor 是我自己实现 BeanPostProcessor 的监控类。
还有个问题:
CGLIB 使用父类代理其实是,新建一个 父类,然后自己创建代理子类,那么 Spring 里面所有注入的(@Autowired @Resource)这样的属性,都会为 NULL!!
这个地方我注释掉了我自己的拷贝方法,我尝试过 BeanCopier(比较流行的HUTOOL工具包,里面有给父类复制属性的),BeanUtils,还有 cglib 自带的 BeanCopier,但全部不符合业务,hutool 的父类属性拷贝,还是会出现 null 的问题,一次 null 都不能有!所以,还是我自己基于自己业务写就好了。
具体代码:
处理的地方 DataSourceAspect ,MySpringBeanProcessor , DBSourceEnhancerInterceptor
先把其他类也贴上来。
PhoneTradeController
package org.angular.test.controller;
import org.angular.test.model.PhoneTradeView;
import org.angular.test.service.PhoneTradeService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import java.util.List;
@Controller
@RequestMapping("/phone/trade")
public class PhoneTradeController {
@Resource
private PhoneTradeService phoneTradeService;
@ResponseBody
@RequestMapping("/getPhoneTradeView")
public List<PhoneTradeView> getPhoneTradeView(String userId, String phoneId) {
return phoneTradeService.getPhoneTradeByUserIdAndPhoneId(userId, phoneId);
}
}
PhoneService:
package org.angular.test.service;
import org.angular.test.annotation.DBSource;
import org.angular.test.dao.PhoneMapper;
import org.angular.test.entity.PhoneEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service("phoneService")
public class PhoneService {
@Autowired
private PhoneMapper phoneMapper;
public PhoneEntity getPhoneOne(String phoneId) {
List<PhoneEntity> resList = getPhoneList(phoneId);
return resList.get(0);
}
@DBSource("angulartest")
public List<PhoneEntity> getPhoneList(String phoneId) {
//phoneDaoSupport.getMysqlList();
return phoneMapper.getPhoneList();
}
@DBSource("springboottest")
public void insertMysqlOne(PhoneEntity phoneEntity) {
phoneMapper.insertMysqlList(phoneEntity);
}
}
PhoneTradeService:
package org.angular.test.service;
import com.mysql.jdbc.StringUtils;
import org.angular.test.Converter.PhoneTradeViewConverter;
import org.angular.test.annotation.DBSource;
import org.angular.test.dao.PhoneTradeMapper;
import org.angular.test.entity.PhoneEntity;
import org.angular.test.entity.PhoneTradeEntity;
import org.angular.test.model.PhoneTradeView;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
@Service
public class PhoneTradeService {
@Autowired
private PhoneService phoneService;
@Autowired
private PhoneTradeMapper phoneTradeMapper;
public List<PhoneTradeView> getPhoneTradeByUserIdAndPhoneId(String userId, String phoneId) {
PhoneEntity phone = phoneService.getPhoneOne(phoneId);
List<PhoneTradeEntity> phoneTradeList = getPhoneTradeList(null, userId, phoneId);
return PhoneTradeViewConverter.converPhoneTradeList(phone, phoneTradeList);
}
/**
* 查询单个交易
*/
public PhoneTradeEntity getPhoneTradeOne(String phoneTradeId, String userId, String phoneId) {
List<PhoneTradeEntity> resList = getPhoneTradeList(phoneTradeId, userId, phoneId);
return resList.get(0);
}
/**
* 查询交易列表
* @param phoneTradeId if null : 查询全部
*/
@DBSource("angulartest")
public List<PhoneTradeEntity> getPhoneTradeList(String phoneTradeId, String userId, String phoneId) {
if(StringUtils.isEmptyOrWhitespaceOnly(userId)) { // 大概定义需要用户ID
return Collections.EMPTY_LIST;
}
return phoneTradeMapper.getPhoneTradeList(phoneTradeId, userId, phoneId);
}
}
PhoneMapper:
package org.angular.test.dao;
import org.angular.test.annotation.DBCheck;
import org.angular.test.entity.PhoneEntity;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface PhoneMapper {
List<PhoneEntity> getOrclList();
@DBCheck
List<PhoneEntity> getPhoneList();
void insertMysqlList(PhoneEntity phoneEntity);
}
PhoneTradeMapper:
package org.angular.test.dao;
import org.angular.test.entity.PhoneTradeEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface PhoneTradeMapper {
List<PhoneTradeEntity> getPhoneTradeList(@Param("phoneTradeId")String phoneTradeId,
@Param("userId")String userId,
@Param("phoneId")String phoneId);
}
PhoneMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.angular.test.dao.PhoneMapper">
<insert id="insertMysqlList" parameterType="org.angular.test.entity.PhoneEntity">
insert into t_phone values (#{phoneId}, #{pNumber}, #{phoneName}, #{createTime})
</insert>
<select id="getOrclList" resultType="org.angular.test.entity.PhoneEntity">
select
PHONE_ID as "phoneId",
PHONE_NAME as "phoneName",
CREATE_TIME as "createTime",
P_NUMBER as "pNumber"
from t_phone
</select>
<select id="getPhoneList" resultType="org.angular.test.entity.PhoneEntity">
select
PHONE_ID as "phoneId",
PHONE_NAME as "phoneName",
CREATE_TIME as "createTime",
P_NUMBER as "pNumber"
from t_phone
<if test="phoneId != null">
where PHONE_ID = #{phoneId}
</if>
</select>
</mapper>
PhoneTradeMapper.xml
t_phone.sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `t_phone`;
CREATE TABLE `t_phone` (
`PHONE_ID` int(11) NOT NULL,
`PHONE_NAME` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`CREATE_TIME` date NULL DEFAULT NULL,
`P_NUMBER` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`PHONE_ID`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `t_phone` VALUES (1, 'angular', '2020-08-04', '111111');
SET FOREIGN_KEY_CHECKS = 1;
PhoneTradeViewConverter
package org.angular.test.Converter;
import org.angular.test.entity.PhoneEntity;
import org.angular.test.entity.PhoneTradeEntity;
import org.angular.test.model.PhoneTradeView;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
public class PhoneTradeViewConverter
{
private static String formatStr = "yyyy-MM-dd HH:mm:ss";
public static List<PhoneTradeView> converPhoneTradeList(PhoneEntity phone,
List<PhoneTradeEntity> phoneTradeList) {
List<PhoneTradeView> res = new ArrayList<>();
for(PhoneTradeEntity phoneTrade : phoneTradeList) {
PhoneTradeView view = new PhoneTradeView();
view.setPhoneEntity(phone);
view.setPhonetradeid(phoneTrade.getPhoneTradeId());
view.setStatus(phoneTrade.getStatus());
view.setStatus_msg(phoneTrade.getStatus_msg());
view.setTradeCreateTime(new SimpleDateFormat(formatStr).format(phoneTrade.getCreateTime()));
res.add(view);
}
return res;
}
}
PhoneTradeView
package org.angular.test.model;
import org.angular.test.entity.PhoneEntity;
public class PhoneTradeView {
private int phonetradeid;
private String status;
private String status_msg;
private PhoneEntity phoneEntity;
private String tradeCreateTime;
public int getPhonetradeid() {
return phonetradeid;
}
public void setPhonetradeid(int phonetradeid) {
this.phonetradeid = phonetradeid;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getStatus_msg() {
return status_msg;
}
public void setStatus_msg(String status_msg) {
this.status_msg = status_msg;
}
public PhoneEntity getPhoneEntity() {
return phoneEntity;
}
public void setPhoneEntity(PhoneEntity phoneEntity) {
this.phoneEntity = phoneEntity;
}
public String getTradeCreateTime() {
return tradeCreateTime;
}
public void setTradeCreateTime(String tradeCreateTime) {
this.tradeCreateTime = tradeCreateTime;
}
@Override
public String toString() {
return "PhoneTradeView{" +
"phonetradeid=" + phonetradeid +
", status='" + status + '\'' +
", status_msg='" + status_msg + '\'' +
", phoneEntity=" + phoneEntity +
", tradeCreateTime=" + tradeCreateTime +
'}';
}
}
PhoneEntity
package org.angular.test.entity;
import java.util.Date;
public class PhoneEntity {
private int phoneId;
private String pNumber;
private String phoneName;
private Date createTime;
public void setPhoneId(int phoneId) {
this.phoneId = phoneId;
}
public void setNumber(String pNumber) {
this.pNumber = pNumber;
}
public void setPhoneName(String phoneName) {
this.phoneName = phoneName;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public int getPhoneId() {
return phoneId;
}
public String getPNumber() {
return pNumber;
}
public String getPhoneName() {
return phoneName;
}
public Date getCreateTime() {
return createTime;
}
@Override
public String toString() {
return "Phone{" +
"phoneId=" + phoneId +
", number=" + pNumber +
", phoneName='" + phoneName + '\'' +
", createTime=" + createTime +
'}';
}
}
t_phonetrade.sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `t_phonetrade`;
CREATE TABLE `t_phonetrade` (
`phonetradeid` int(10) NOT NULL AUTO_INCREMENT,
`userid` int(10) NOT NULL,
`phoneId` int(10) NOT NULL,
`status` int(5) NULL DEFAULT NULL,
`status_msg` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`create_time` date NULL DEFAULT NULL,
PRIMARY KEY (`phonetradeid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `t_phonetrade` VALUES (1, 1, 1, 10, '交易已完成', '2020-08-27');
SET FOREIGN_KEY_CHECKS = 1;
大概处理好 Controller Service Mapper Mapper.xml 这些一连串的测试需要之后,就可以开启两个核心类编写:
DBSourceEnhancerInterceptor
package org.angular.test.interceptor;
import com.p6spy.cglib.proxy.MethodProxy;
import org.angular.test.annotation.DBSource;
import org.angular.test.conf.DbContextHolder;
import com.p6spy.cglib.proxy.MethodInterceptor;
import org.aspectj.lang.JoinPoint;
import org.springframework.context.ApplicationContext;
import java.lang.reflect.Method;
public class DBSourceEnhancerInterceptor implements MethodInterceptor {
private ApplicationContext context; // 后续使用
private static final org.slf4j.Logger LOGGER = org.slf4j.LoggerFactory.getLogger(DBSourceEnhancerInterceptor.class);
public DBSourceEnhancerInterceptor(ApplicationContext context) {
this.context = context;
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
if(!method.isAnnotationPresent(DBSource.class)) {
return methodProxy.invokeSuper(o, objects);
}
// 将 DataSourceAspect 的逻辑延用
String dataSource = method.getAnnotation(DBSource.class).value();
LOGGER.info("change to >>>> " + dataSource + " <<<< Source...");
DbContextHolder.setDbType(dataSource);
Object res = methodProxy.invokeSuper(o, objects);
// 后续处理
afterSwitchDS();
return res;
}
/**
* 用于调用完方法后续处理
*/
public void afterSwitchDS(){
DbContextHolder.clearDbType();
}
}
MySpringBeanProcessor
package org.angular.test.conf;
import org.angular.test.annotation.DBSource;
import org.angular.test.interceptor.DBSourceEnhancerInterceptor;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import com.p6spy.cglib.proxy.Enhancer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class MySpringBeanProcessor implements BeanPostProcessor, ApplicationContextAware
{
private ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String s) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(final Object bean, String beanName) throws BeansException {
Method[] methods = bean.getClass().getMethods();
// 对注解类进行增强
for(Method method : methods) {
if (method.isAnnotationPresent(DBSource.class)) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(bean.getClass());
enhancer.setCallback(new DBSourceEnhancerInterceptor(context));
Object res = enhancer.create();
copyAction(res, bean);
return res;
}
}
return bean;
}
public void copyAction(Object target, Object origin) {
Field[] targetFields = target.getClass().getSuperclass().getDeclaredFields();
Class originClass = origin.getClass();
Map<String, Field> originMap = convertObjectToMap(originClass);
try{
for(Field targetField : targetFields) {
if(!Modifier.isFinal(targetField.getModifiers()) && originMap.containsKey(targetField.getName())) {
targetField.setAccessible(true);
Field originField = originMap.get(targetField.getName());
originField.setAccessible(true);
targetField.set(target, originField.get(origin));
targetField.setAccessible(false);
originField.setAccessible(false);
}
}
} catch (Exception e) {
return ;
}
}
public Map<String, Field> convertObjectToMap(Class originClass) {
Map<String, Field> res = new HashMap<String, Field>();
Field[] fields = originClass.getDeclaredFields();
for(Field field : fields) {
res.put(field.getName(), field);
}
return res;
}
}
最后进行测试
http://localhost:8088/angularTest/phone/getPhoneList 访问是否能够正常,因为如果没有 CGLIB 的内部调用拦截,就会导致注解空,从而不能访问数据库
http://localhost:8088/angularTest/phone/insertMysqlOne 另外一个接口,可以自行编写另外一个动态数据源测试。
当然我自己测试都可以了,既能够动态切换数据源,也能够通过 CGLIB 解决 Spring AOP 内部调用问题,可喜可贺。