作为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注解的时候,一定要注意这些细节,避免注解失效而让程序达不到预期的效果甚至出行严重的事故。