热爱技术,追求卓越
不断求索,精益求精

秒懂java,动态代理、aop、@Async、@Transactianal等的坑,不小心java注解就失效了

作为java研发人员,什么注解编程、接口编程、切面编程,每一个出来都说得头头是道。但是你真的会用注解、接口、切面这些东西嘛?你在自己的日常研发过程中有没有踩过坑?一谈到注解,就是什么动态代理、aop啥的讲得天花乱坠,你是否遇到过注解失效的问题,你又是如何解决的呢?

我们以spring/spring boot框架中常用的两个注解@Async(异步)、@Transactianal(事务)来说明一下。下面的TestService中testAnnotation会调用含有注解@Async和注解@Transactianal的方法testAsyncAndTransactionalAnnotation,在testAsyncAndTransactionalAnnotation中会抛出一个运行时异常,当我们调用testAnnotation方法的时候,我们的预期是主线程ID和异步线程ID不同并且数据库不会插入成功。

package cn.lovecto.promotion.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import cn.lovecto.promotion.dao.mapper.OperationLogMapper;
import cn.lovecto.promotion.model.OperationLog;

@Service
public class TestService implements ITestService{

    @Autowired
    private OperationLogMapper logMapper;

    /**
     * 测试注解
     * @return
     * @throws InterruptedException 
     */
    @Override
    public void testAnnotation() throws InterruptedException{
        System.out.println("主线程ID:" + Thread.currentThread().getId());
        testAsyncAndTransactionalAnnotation();
    }

    @Async//加入异步注解
    @Transactional//加入事务注解
    @Override
    public void testAsyncAndTransactionalAnnotation() throws InterruptedException{
        System.out.println("异步线程ID:" + Thread.currentThread().getId());
        //数据库操作,插入一条记录,可以换成任意的数据库写操作
        OperationLog record = new OperationLog(1, 1, (byte)1, 1, "测试用");
        logMapper.insert(record);
        throw new RuntimeException("故意抛出一个异常");
    }
}

看我们的应用启动类AppTest,有注解@EnableAsync和@EnableTransactionManagement以及@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)。异步支持、事务支持、动态代理支持都开启了。

package cn.lovecto.promotion;

import org.apache.log4j.PropertyConfigurator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import tk.mybatis.spring.annotation.MapperScan;

@EnableAutoConfiguration
@ComponentScan(basePackages = "cn.lovecto.promotion")
@MapperScan(basePackages = "cn.lovecto.promotion.dao.mapper")
@EnableScheduling
@EnableAsync
@EnableTransactionManagement
@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)
public class AppTest {
    public static void main(String[] args) {
        PropertyConfigurator.configure(System.getProperty("logging.config",
                "log4j.properties"));
        SpringApplication.run(AppTest.class, args);
    }
}

来看看我们的测试方法,直接调用TestService的testAnnotation方法。

package cn.lovecto.promotion.service;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import cn.lovecto.promotion.AppTest;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = AppTest.class)
public class TestServiceTest {

    @Autowired
    private TestService testService;

    @Test
    public void test() throws InterruptedException{
        testService.testAnnotation();
        Thread.sleep(10000);
    }
}

运行结果让人大跌眼镜啊!

主线程ID:1
异步线程ID:1

什么,主线程和异步线程居然是同一个线程?“这一定是spring框架的bug?”,再看数据库,我去,居然成功插入了一条记录,不是抛了异常,事务要回滚么?“spring也有不靠谱的时候啊!”。

是不是特别疑惑?是的,特别疑惑!原因是我们一谈到注解、一谈到aop就只停留在会使用的层面,以为加上了注解就可以放心的让框架去处理了,其实离真正的会使用还很远呐。

出现上面的非预期结果是因为我们在TestService同一个类中testAnnotation方法调用了有注解@Async和注解@Transactianal的方法。spring注解/Spring AOP的原理就是动态代理,他的代理有两种,分别是CGLB和JDK自带的代理,Spring AOP会根据具体的实现不同,采用不同的代理方式。在testAnnotation方法调用testAsyncAndTransactionalAnnotation方法时,默认隐藏了关键字this,其实调用是这样的:

this.testAsyncAndTransactionalAnnotation();

此时的调用并不是代理调用,而是一个对象调用,因为当你在同一个类中,方法调用是在实体内执行的,spring无法截获到这个方法调用。所以在同一个类中调用添加注解的方法时就会失效。也就是说调用的对象是当前对象,当前对象是TestService,问题就出在这里,调用testAsyncAndTransactionalAnnotation,必须用代理对象执行,因为代理对象要做异步相关的增强,但是此时却直接用当前对象TestService对象调用,绕过了代理对象增强的部分,也就是说代理增强部分失效。事务增强部分也一样失效了。

怎么办,有两种解决办法,如果类是接口的实现类(若ITestServic),像下面这样修改testAnnotation方法:

@Override
public void testAnnotation() throws InterruptedException{
    System.out.println("主线程ID:" + Thread.currentThread().getId());
    ITestService service = (ITestService)AopContext.currentProxy();
    service.testAsyncAndTransactionalAnnotation();
}

运行结果如下,数据库也未插入数据,达到预期结果。

主线程ID:1
异步线程ID:47

另外一种办法是把带有注解的方法移到另外一个类中,其他类调用时候就会使用spring动态代理增强。比如我们把testAnnotation类移到TestProxy类中:

package cn.lovecto.promotion.proxy;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import cn.lovecto.promotion.dao.mapper.OperationLogMapper;
import cn.lovecto.promotion.model.OperationLog;

@Component
public class TestProxy {

    @Autowired
    private OperationLogMapper logMapper;

    @Async
    @Transactional
    public void testAsyncAndTransactionalAnnotation() throws InterruptedException{
        System.out.println("异步线程ID:" + Thread.currentThread().getId());
        OperationLog record = new OperationLog(1, 1, (byte)1, 1, "测试用");
        logMapper.insert(record);
        throw new RuntimeException("故意抛出一个异常");
    }

}

修改后的TestService类:

package cn.lovecto.promotion.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import cn.lovecto.promotion.proxy.TestProxy;

@Service
public class TestService {

    @Autowired
    private TestProxy proxy;

    /**
     * 测试注解
     * @return
     * @throws InterruptedException 
     */
    public void testAnnotation() throws InterruptedException{
        System.out.println("主线程ID:" + Thread.currentThread().getId());
        proxy.testAsyncAndTransactionalAnnotation();
    }
}

执行结果也能达到预期,数据库事务也达到了预期。

主线程ID:1
异步线程ID:47

所以,在使用注解编程的时候,我们不要停留在浅层次的使用,至少要知道动态代理的原理,这样在日常研发过程中才不至于出现问题后不知道原因。在使用spring/spring boot的aop的过程中,如使用@Async、@Transactianal等java注解的时候,一定要注意这些细节,避免注解失效而让程序达不到预期的效果甚至出行严重的事故。

赞(3)
未经允许不得转载:LoveCTO » 秒懂java,动态代理、aop、@Async、@Transactianal等的坑,不小心java注解就失效了

热爱技术 追求卓越 精益求精