SpringBoot学习示例—AOP切面编程在内部方法调用时候切面逻辑失效问题(已解决,附完整项目代码)


作者:空白

1. 切面逻辑失效

切面编程方式来做增强逻辑,在我们系统中很多场景都有在使用,例如事务@Transaction,缓存@Cacheable,以及自定义AOP注解等。在这里,大家都会遇到这样的一个场景和问题:service层的类里,调用同一个类的内部方法,特别是事务AOP注解,就会出现事务失效的情况。

还有其他应用场景,AOP失效的问题会带来非常大的开发设计成本。

  • 多数据源场景,后台管理系统的数据统计功能,需要从各个库中获取数据来计算并做出图表,这种场景就需要不断切换切换数据源,来获取这些在不同数据库中的数据。

  • 读写分离场景,同一个业务类中方法是放在一起的,读方法和写方法都是在同一个类中,一般是不会分拆到不同类中维护的。那么这个时候内部读写方法相互切换,如果AOP内部调用失效问题无法解决,就会非常麻烦。

本文测试案例

测试案例就是在多数据源的使用场景,两个库:master库和slave库。两个查询方法select1和select2,分别是从主库和从库中查询出数据。

select1调用select2方法。运行后发现select2查询出的是主库的数据。数据源切换AOP注解没有生效。

@Service
public class UserService2 implements SelfBeanProxyAware{

    @Autowired
    private UserMapper userMapper;
    
    private UserService2 userServiceProxy;

    @DBType(DataSourceType.SLAVE)
    public void select(){
        User user = userMapper.selectByPrimaryKey(2);
        System.out.println(user.getId() + "--" + user.getName() + "==" + user.getGender());
    }

    @DBType(DataSourceType.MASTER)
    public void select1() {
        User user = userMapper.selectByPrimaryKey(10);
        System.out.println("MASTER:"+user.getId() + "--" + user.getName() + "==" + user.getGender());
        select2();
    } 

    @DBType(DataSourceType.SLAVE)
    public void select2() {
    	User user = userMapper.selectByPrimaryKey(10);
        System.out.println("SLAVE:"+user.getId() + "--" + user.getName() + "==" + user.getGender());
    }
    
    @DBType(DataSourceType.MASTER)
    public void select3() {
        User user = userMapper.selectByPrimaryKey(10);
        System.out.println("MASTER:"+user.getId() + "--" + user.getName() + "==" + user.getGender());
        userServiceProxy.select2();
    } 
 
    

    @DBType(DataSourceType.MASTER)
    public void insert(User user){
        userMapper.insertSelective(user);
    }

	@Override
	public void setSelBeanfProxy(Object proxyObj) {
		// TODO Auto-generated method stub
		this.userServiceProxy = (UserService2)proxyObj;
	}
}

2. 问题定位

对于问题如何定位,这里我自定义的数据源切换逻辑,是会打印出日志的,如果增强逻辑打印出日志了,那么表示增强逻辑执行了,如果没有则表示切面逻辑没有生效。

    @Before("@annotation(dbType)")
    public void changeDataSourceType(JoinPoint joinPoint, DBType dbType){
    	
        DataSourceType curType = dbType.value();
        if(!DynamicDataSourceHolder.containsType(curType)){
            logger.info("指定数据源[{}]不存在,使用默认数据源-> {}",dbType.value(),joinPoint.getSignature());
        }else{
            logger.info("use datasource {} -> {}",dbType.value(),joinPoint.getSignature());
            DynamicDataSourceHolder.setType(dbType.value());
        }

    }

运行后,发现该行日志并没有打印。

3. 问题分析

对于AOP失效问题,我们就得了结Spring的内部机制。系统的bean对象,注册到spring容器中,是存在两种情况的,不同的引用对应这不同的方法区的方法。

@Autowired注解是从容器中获取类的代理对象,因此我们调用的也是代理类的方法,而内部之间调用,使用的是对象的内部this指针,指向的是类方法的方法区、即没有AOP注解的增强逻辑。

4. 解决方案

解决方案的主要思路是:不使用内部this指针调用内部方法,引用从容器中获得,对象引用换成容器中的代理对象引用。如下代码所示:

    // 注入当前Bean使得调用内部方法也被SpringAOP拦截
    UserService service = SpringContextUtil.getBean(this.getClass());
    User user = service .selectById(id);

这种情况下,每次调用都要多这几行代码,而且每次调用都要重复写这些代码。本demo项目提供了更简洁的自动化注入代理对象的方案。

  1. 将整个包proxyautowired拷贝到你的项目中去,该包能为每个实现了接口SelfBeanProxyAware的类自动化注入代理对象。将注册代理对象的过程抽取出来了。

  2. 维护一个本类的引用字段。

  3. 类实现一个接口SelfBeanProxyAware,并实现public void setSelBeanfProxy(Object proxyObj)方法。

可能会有开发者说,这样不是加大了开发工作量吗,起始不然,业务复杂情况下,类内部方法的相互调用情况非常多,而且这样的类会非常多,通过统一的方式就能将所有内部调用情况统一管理,从成本角度出发,是非常划算的。

5. 测试方案效果

启动项目测试select3方法,select3查出主库的数据,再通过代理对象调用select2方法,成功切换到从库,并查出从库数据。

完整的demo项目,请关注公众号“前沿科技bot“并发送"状态机"获取。

alt

扫码或搜索:前沿科技
发送 290992
即可立即永久解锁本站全部文章