快速cglib替代AOP解决内部调用

当Spring AOP在4.1.2版本中遇到内部调用时,AOP切面无法生效。本文介绍了如何通过CGLIB的methodProxy.invokeSuper方法来解决这个问题,同时指出在使用CGLIB代理时,Spring注入的属性可能会为NULL,并探讨了属性拷贝的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上级文章:

快速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 内部调用问题,可喜可贺。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值