一、前言

  Spring AOP在使用过程中需要注意一些问题,也就是平时我们说的陷阱,这些陷阱的出现是由于Spring AOP的实现方式造成的。每一样技术都或多或少有它的局限性,很难称得上完美,只要掌握其实现原理,在使用时不要掉进陷阱就行,也就是进行规避。

对于Spring AOP的陷阱,我总结了以下两个方面,现在分别进行介绍。

二、各种AOP失败场景

2.1、(public)方法被嵌套使用而失效

Service中的方法调用同Service中的另一个方法时,如此调用并非调用的是代理类中的方法,是不会被切进去的。换言之,必须要调用代理类才会被切进去。 那么应该怎么破呢?既然只有调用代理类的方法才能切入,那我们拿到代理类不就好了嘛。尝试性的在IDE里面搜Aop相关的类,一眼就看到一个叫AopContext的东西,看来游戏啊,里面有一个方法叫做currentProxy(),返回一个Object。但这样做,需要修改Spring的默认配置expose-proxy="true"。

2.1.1、 问题场景

通过例子来讲解这样更好,首先加上注解配置:

<!-- 启用注解式AOP -->
<aop:aspectj-autoproxy/>

然后定义一个切面,代码如下:

@Aspect
@Component
public class AnnotationAspectTest { @Pointcut("execution(* *.action(*))")
public void action() {
} @Pointcut("execution(* *.work(*))")
public void work() {
} @Pointcut("action() || work())")
public void compositePointcut() {
} //前置通知
@Before("compositePointcut()")
public void beforeAdvice() {
System.out.println("before advice.................");
} //后置通知
@After("compositePointcut()")
public void doAfter() {
System.out.println("after advice..................");
}
}

测试代码:

//定义接口
public interface IPersonService {
String action(String msg); String work(String msg);
} //编写实现类
@Service
public class PersonServiceImpl implements IPersonService { public String action(String msg) {
System.out.println("FooService, method doing."); this.work(msg); // *** 代码 1 *** return "[" + msg + "]";
} @Override
public String work(String msg) {
System.out.println("work: * " + msg + " *");
return "* " + msg + " *";
}
}

//单元测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class FooServiceTest { @Autowired
private IPersonService personService; @Test
public void testAction() {
personService.action("hello world.");
}
}

测试结果:

说明嵌套在action方法内部的work方法没有被进行切面增强,它没有被“切中”。

2.1.2、 解决方案

A、在实现类中,如果注释掉代码1,将代码1改为:

((IPersonService) AopContext.currentProxy()).work(msg); // *** 代码 2 ***

B、并且在XML配置中加上expose-proxy="true",变为:<aop:aspectj-autoproxy expose-proxy="true"/>。或者在spring-boot中,目前都是通过annotation来代替配置文件的,所以我们必须找到一个annotation来代替这段配置,发现在ApplicationMain中加入@EnableAspectJAutoProxy(proxyTargetClass=true),需要增加spring-boot-starter-aop的依赖。

运行结果为:

嵌套在action方法内部的work方法被进行了切面增强,它被“切中”。

2.1.3、 原因分析

2.1.3.1 原理
以上结果的出现与Spring AOP的实现原理息息相关,由于Spring AOP采用了动态代理实现AOP,在Spring容器中的bean(也就是目标对象)会被代理对象代替,代理对象里加入了我们需要的增强逻辑,当调用代理对象的方法时,目标对象的方法就会被拦截。而上文中问题出现的症结也就是在这里,通过调用代理对象的action方法,在其内部会经过切面增强,然后方法被发射到目标对象,在目标对象上执行原有逻辑,如果在原有逻辑中嵌套调用了work方法,则此时work方法并没有被进行切面增强,因为此时它已经在目标对象内部。

而解决方案很好地说明了,将嵌套方法发射到代理对象,这样就完成了切面增强。

2.1.3.2 源代码分析
接下来我们简单看一下源代码,Spring AOP的代码逻辑相当清晰:

/**
* Implementation of {@code InvocationHandler.invoke}.
* <p>Callers will see exactly the exception thrown by the target,
* unless a hook method throws an exception.
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
... ... Object retVal; //*** 代码3 ***
if (this.advised.exposeProxy) {
// Make invocation available if necessary.
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
} ... ... // Get the interception chain for this method.
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); // Check whether we have any advice. If we don't, we can fallback on direct
// reflective invocation of the target, and avoid creating a MethodInvocation.
if (chain.isEmpty()) {
// We can skip creating a MethodInvocation: just invoke the target directly
// Note that the final invoker must be an InvokerInterceptor so we know it does
// nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, args);
}
else {
// We need to create a method invocation...
invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// Proceed to the joinpoint through the interceptor chain.
retVal = invocation.proceed();
} ... ...
}

在代码3处,如果配置了exposeProxy开关,则会将代理对象暴露在当前线程中,以供其它需要的地方使用。那么是怎么暴露的呢?答案很简单,通过使用静态的全局ThreadLocal变量就解决了问题。

2.2、Spring事务在多线程环境下失效

2.2.1 问题场景

沿用上面的代码稍作修改,加上事务配置:

<!-- 数据库的事务管理器配置 -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="meilvDataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>

代码如下所示:

@Service
@Transactional(propagation = Propagation.REQUIRED, timeout = 10000000)
public class PersonServiceImpl implements IPersonService { @Autowired
IUserDAO userDAO; @Override
public String action(final String msg) { new Thread(new Runnable() {
@Override
public void run() {
(getThis()).work(msg);
}
}).start(); UserDO userDO = new UserDO();
userDO.setName("lanlan");
userDAO.insert(userDO); return "[" + msg + "]";
} @Override
public String work(String msg) {
System.out.println("work: * " + msg + " *");
UserDO userDO = new UserDO();
userDO.setName("yanyan");
userDAO.insert(userDO); throw new RuntimeException();
} private IPersonService getThis() {
try {
return (IPersonService) AopContext.currentProxy();
} catch (IllegalStateException e) {
return this;
}
}
}

结果:work方法中抛出异常,但是没有影响事务的回滚,说明事务在子线程中失效了。

2.2.2 解决方案

只需要将多线程中的方法提出来,或者作为另一个Service类中的方法即可。

@Service
@Transactional(propagation = Propagation.REQUIRED, timeout = 10000000)
public class PersonServiceImpl implements IPersonService { @Autowired
IUserDAO userDAO; @Override
public String action(final String msg) { (getThis()).work(msg); UserDO userDO = new UserDO();
userDO.setName("lanlan");
userDAO.insert(userDO); return "[" + msg + "]";
} @Override
public String work(String msg) {
System.out.println("work: * " + msg + " *");
UserDO userDO = new UserDO();
userDO.setName("yanyan");
userDAO.insert(userDO); throw new RuntimeException();
} private IPersonService getThis() {
try {
return (IPersonService) AopContext.currentProxy();
} catch (IllegalStateException e) {
return this;
}
}
}

上面只是一个简单的例子,用于进行问题说明。

a、如果去掉多线程,将方法放在同一个类里,Spring则会根据事务的传播配置参数,是否重新启用新的事务。

b、如果将方法独立出来放在新的类里,并且该方法也配置了事务,则会重新启用新的事务。

2.2.3 原因分析

Spring的事务处理为了与数据访问解耦,它提供了一套处理数据资源的机制,而这个机制与上文中的原理相差无几,也是采用的ThreadLocal的方式。

在编程中,Service实例都是单例的无状态的,事务管理则需要加入事务控制的相关状态变量,使得Service实例不再是无状态线程安全的,解决这个问题的方式就是使用ThreadLocal。

通过使用ThreadLocal将数据源绑定在当前线程上,在当前线程的事务中,从设定的地方去取连接就会是同一个数据库连接,这样操作事务就会在同一个连接上进行。

但是,ThreadLocal的特性是,绑定在当前线程中的变量不会自动传递到其它线程中(当然,InheritableThreadLocal可以在父子线程中间传递变量值,但是这需要特殊的使用场景),所以当开启子线程时,子线程并没有父线程的数据库连接资源。

对于上文提到的陷阱:如果另外开启线程,那么在新线程中将获取不到父线程的连接,事务要么失效,要么重新开启一个新的。

源代码如下:

public abstract class DataSourceUtils {

    public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
try {
return doGetConnection(dataSource);
}
catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
}
} public static Connection doGetConnection(DataSource dataSource) throws SQLException {
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
conHolder.requested();
if (!conHolder.hasConnection()) {
logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(dataSource.getConnection());
}
return conHolder.getConnection();
} Connection con = dataSource.getConnection(); //...... return con;
}
} public abstract class TransactionSynchronizationManager { private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<Map<Object, Object>>("Transactional resources"); /**
* Retrieve a resource for the given key that is bound to the current thread.
* @param key the key to check (usually the resource factory)
* @return a value bound to the current thread (usually the active
* resource object), or {@code null} if none
* @see ResourceTransactionManager#getResourceFactory()
*/
public static Object getResource(Object key) {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
Object value = doGetResource(actualKey);
if (value != null && logger.isTraceEnabled()) {
logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" +
Thread.currentThread().getName() + "]");
}
return value;
} /**
* Actually check the value of the resource that is bound for the given key.
*/
private static Object doGetResource(Object actualKey) {
Map<Object, Object> map = resources.get();
if (map == null) {
return null;
}
Object value = map.get(actualKey);
// Transparently remove ResourceHolder that was marked as void...
if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {
map.remove(actualKey);
// Remove entire ThreadLocal if empty...
if (map.isEmpty()) {
resources.remove();
}
value = null;
}
return value;
}
}

2.3、Spring Cache失效(同2.1aop类内部调用拦截失效相同)

我们知道缓存方法的调用是通过spring aop切入的调用的。在一个类调用另一个类中的方法可以直接的简单调用,但是如果在同一个类中调用自己已经通过spring托管的类中的方法该如何实现呢?

先来段代码:

public List<Long> getSkuIdsBySpuId(long spuId) {
ItemComposite itemComposite = this.getItemComposite(spuId);///能走下面的缓存吗?
if (itemComposite!=null) {
if ( CollectionUtils.isNotEmpty(itemComposite.getItemSkus())) {
return itemComposite.getItemSkus().stream().map(itemSku -> itemSku.getId()).collect(Collectors.toList());
}
}
return Collections.emptyList();
} @Cacheable(value = "getItemComposite", key = "#spuId")
public ItemComposite getItemComposite(long spuId) {
//select from db...
}

结果是这种方式是无法走到下面的getItemComposite缓存方法的,原因就是上面说的类内部无法通过直接调用方法来调用spring托管的bean,必须在当前类中拿到其代理类。通过查找资料修改如下:

public List<Long> getSkuIdsBySpuId(long spuId) {
ItemCacheManager itemCacheManager = (ItemCacheManager)AopContext.currentProxy();
if (itemComposite!=null) {
if ( CollectionUtils.isNotEmpty(itemComposite.getItemSkus())) {
return itemComposite.getItemSkus().stream().map(itemSku -> itemSku.getId()).collect(Collectors.toList());
}
}
return Collections.emptyList();
} @Cacheable(value = "getItemComposite", key = "#spuId")
public ItemComposite getItemComposite(long spuId) {
//select from db...
}

可以看到修改的地方是通过调用AopContext.currentProxy的方式去拿到代理类来调用getItemComposite方法。这样就结束了?不是,通过调试发现会抛出异常:java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.

继续查找资料,csdn上一篇文章正好有篇文章http://blog.csdn.net/z69183787/article/details/45622821是讲述这个问题的,他给的解决方法是在applicationContext.xml中添加一段<aop:aspectj-autoproxy proxy-target-class="true"expose-proxy="true"/>。但是与我们的系统不同的是,我们系统是通过spring-boot来启动的,目前都是通过annotation来代替配置文件的,所以我们必须找到一个annotation来代替这段配置,发现在ApplicationMain中加入@EnableAspectJAutoProxy(proxyTargetClass=true)然后添加maven依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

可以解决我们的问题,这时候你一定认为事情可以大功告成了,但是真正的坑来了:我们的spring-boot版本是1.3.5,版本过低,这种注解必须是高版本才能支持。

还是想想csdn上的那篇文章,通过配置文件是可以解决的,那么我们就在spring boot中导入配置文件应该就没问题了啊。

于是我们可以配置一个aop.xml文件,文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> <aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/> </beans>

然后在ApplicationMain中添加注解如下:
@ImportResource(locations = "aop.xml")
OK.

2.4、@Async失效

在同一个类中,一个方法调用另外一个有注解(比如@Async,@Transational)的方法,注解是不会生效的。

比如,下面代码例子中,有两方法,一个有@Async注解,一个没有。第一次如果调用了有注解的test()方法,会启动@Async注解作用;第一次如果调用testAsync(),因为它内部调用了有注解的test(),如果你以为系统也会为它启动Async作用,那就错了,实际上是没有的。

    @Service
public class TestAsyncService { public void testAsync() throws Exception {
test();
} @Async
public void test() throws InterruptedException{
Thread.sleep(10000);//让线程休眠,根据输出结果判断主线程和从线程是同步还是异步
System.out.println("异步threadId:"+Thread.currentThread().getId());
}
}

运行结果:testAsync()主线程和从线程()test()从线程同步执行。 
原因:spring 在扫描bean的时候会扫描方法上是否包含@Async注解,如果包含,spring会为这个bean动态地生成一个子类(即代理类,proxy),代理类是继承原来那个bean的。此时,当这个有注解的方法被调用的时候,实际上是由代理类来调用的,代理类在调用时增加异步作用。然而,如果这个有注解的方法是被同一个类中的其他方法调用的,那么该方法的调用并没有通过代理类,而是直接通过原来的那个bean,所以就没有增加异步作用,我们看到的现象就是@Async注解无效。

三.aop类内部调用拦截失效的解决方案

3.1 方案一--从beanFactory中获取对象

刚刚上面说到controller中的UserService是代理对象,它是从beanFactory中得来的,那么service类内调用其他方法时,也先从beanFacotry中拿出来就OK了。

public void insert02(User u){
getService().insert01(u);
}
private UserService getService(){
return SpringContextUtil.getBean(this.getClass());
}

3.2 方案二--获取代理对象

private UserService getService(){

    // 采取这种方式的话,
//@EnableAspectJAutoProxy(exposeProxy=true,proxyTargetClass=true)
//必须设置为true
return AopContext.currentProxy() != null ? (UserService)AopContext.currentProxy() : this;
}

如果aop是使用注解的话,那需要@EnableAspectJAutoProxy(exposeProxy=true,proxyTargetClass=true),如果是xml配置的,把expose-proxy设置为true,如

<aop:config expose-proxy="true">
<aop:aspect ref="XXX">
<!-- 省略--->
</aop:aspect>
</aop:config>

3.3方案三--将项目转为aspectJ项目

将项目转为aspectJ项目,aop转为aspect 类。

spring AOP 之二:@AspectJ注解的3种配置

3.4 方案四--BeanPostProcessor

通过BeanPostProcessor 在目标对象中注入代理对象,定义InjectBeanSelfProcessor类,实现BeanPostProcessor。也不具体写了

spring AOP 之二:@AspectJ注解的3种配置

关于AOP无法切入同类调用方法的问题的更多相关文章

  1. spring aop 动态代理批量调用方法实例

    今天项目经理发下任务,需要测试 20 个接口,看看推送和接收数据是否正常.因为对接传输的数据是 xml 格式的字符串,所以我拿现成的数据,先生成推送过去的数据并存储到文本,以便验证数据是否正确,这时候 ...

  2. 相同类中方法间调用时日志Aop失效处理

    本篇分享的内容是在相同类中方法间调用时Aop失效处理方案,该问题我看有很多文章描述了,不过大多是从事务角度分享的,本篇打算从日志aop方面分享(当然都是aop,失效和处理方案都是一样),以下都是基于s ...

  3. SpringBoot中使用AOP打印接口日志的方法(转载)

    前言 AOP 是 Aspect Oriented Program (面向切面)的编程的缩写.他是和面向对象编程相对的一个概念.在面向对象的编程中,我们倾向于采用封装.继承.多态等概念,将一个个的功能在 ...

  4. Spring源码分析之AOP从解析到调用

    正文: 在上一篇,我们对IOC核心部分流程已经分析完毕,相信小伙伴们有所收获,从这一篇开始,我们将会踏上新的旅程,即Spring的另一核心:AOP! 首先,为了让大家能更有效的理解AOP,先带大家过一 ...

  5. Spring AOP在函数接口调用性能分析及其日志处理方面的应用

    面向切面编程可以实现在不修改原来代码的情况下,增加我们所需的业务处理逻辑,比如:添加日志.本文AOP实例是基于Aspect Around注解实现的,我们需要在调用API函数的时候,统计函数调用的具体信 ...

  6. Spring AOP基于配置文件的面向方法的切面

    Spring AOP基于配置文件的面向方法的切面 Spring AOP根据执行的时间点可以分为around.before和after几种方式. around为方法前后均执行 before为方法前执行 ...

  7. 关于spring的aop拦截的问题 protected方法代理问题

    看到一篇很好的Spring aop 拦截方法的问题,  原文地址. 问题 貌似不能拦截私有方法? 试了很多次,都失败了,是不是不行啊? 我想了一下,因为aop底层是代理, jdk是代理接口,私有方法必 ...

  8. 【事务】<查询不到同一调用方法其它事务提交的更新>解决方案

    最近遇到一个很棘手的问题,至今也解释不清楚原因,不过已经找到了解决方案. 先来看看Propagation属性的值含义,@Transactional中Propagation属性有7个选项可供选择: Pr ...

  9. 利用C#实现AOP常见的几种方法详解

    利用C#实现AOP常见的几种方法详解 AOP面向切面编程(Aspect Oriented Programming) 是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术. 下面这篇文章主要 ...

随机推荐

  1. hexo + Github 搭建问题综述

    1.Mac下安装hexo Error: Cannot find module './build/Release/DTraceProviderBindings 解决: solution 2.node s ...

  2. Qt5_pro_02

    1.g++ 编译参数 如果 用g++编译时,命令行是这样的:“g++ main.cpp -std=c++0x -pthread” 则在Qt的pro文件中这样设置: QMAKE_CXXFLAGS += ...

  3. Java网络编程学习A轮_08_NIO的Reactor模型

    参考资料: 了解 Java NIO 的 Reactor 模型,大神 Doug Lea 的 PPT Scalable IO in Java 必看:http://gee.cs.oswego.edu/dl/ ...

  4. mongo学亮的分享

    # MongoDB 集群部署## 关键词* 集群* 副本集* 分片## MongoDB集群部署>今天主要来说说Mongodb的三种集群方式的搭建Replica Set副本集 / Sharding ...

  5. HDU1565 方格取数 &&uva 11270 轮廓线DP

    方格取数(1) Time Limit: 10000/5000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Su ...

  6. 创建对象并生成结果的3个步骤-Chapter 3 P38

    必须完成3个步骤才能创建对象并生成结果: 1 创建对象   namespace LanguageFeatures { public class Product { public int Product ...

  7. CC工具列表

    QuasarRAT   Adwind Adzok Arcom Babylon Blacknix Blue Banana Bozok Coringa DarkComet DRAT Gh0st Huige ...

  8. 利用Pandoc将markdown文件转化为pdf

    利用Pandoc将markdown文件转化为pdf 准备工作 安装pandoc 安装MiKTeX 将markdown文件转换为pdf 准备工作 安装pandoc Windows下安装pandoc很容易 ...

  9. java之double类型数值的比较

    先看demo: public class L26 { /** * @param args */ public static void main(String[] args) { // TODO Aut ...

  10. (转)基于DDD的现代ASP.NET开发框架--ABP分层架构

    介绍DDD概念Eric Evans的“Domain-Driven Design领域驱动设计”简称 DDD,它是一套综合软件系统分析和设计的面向对象建模方法,或者可称为MDD模型驱动方法的一种,区别于M ...