这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

AOP是Spring提供的关键特性之一。AOP即面向切面编程,是OOP编程的有效补充。

使用AOP技术,可以将一些系统性相关的编程工作,独立提取出来,独立实现,然后通过切面切入进系统。

从而避免了在业务逻辑的代码中混入很多的系统相关的逻辑——比如权限管理,事物管理,日志记录等等。

这些系统性的编程工作都可以独立编码实现,然后通过AOP技术切入进系统即可。从而达到了 将不同的关注点分离出来的效果。

本文深入剖析Spring的AOP的原理。

1. AOP相关的概念

1) Aspect :切面,切入系统的一个切面。比如事务管理是一个切面,权限管理也是一个切面;

2) Join point :连接点,也就是可以进行横向切入的位置;

3) Advice :通知,切面在某个连接点执行的操作(分为: Before advice , After returning advice , After throwing advice , After (finally) advice , Around advice );

4) Pointcut :切点,符合切点表达式的连接点,也就是真正被切入的地方;

2. AOP 的实现原理

AOP分为静态AOP和动态AOP。

静态AOP是指AspectJ实现的AOP,他是将切面代码直接编译到Java类文件中。

动态AOP是指将切面代码进行动态织入实现的AOP。

Spring的AOP为动态AOP,实现的技术为: JDK提供的动态代理技术 和 CGLIB(动态字节码增强技术) 。尽管实现技术不一样,但 都是基于代理模式 , 都是生成一个代理对象 。

1) JDK动态代理

主要使用到 InvocationHandler 接口和 Proxy.newProxyInstance() 方法。

JDK动态代理要求被代理实现一个接口,只有接口中的方法才能够被代理 。

其方法是将被代理对象注入到一个中间对象,而中间对象实现InvocationHandler接口,

在实现该接口时,可以在 被代理对象调用它的方法时,在调用的前后插入一些代码。

而 Proxy.newProxyInstance() 能够利用中间对象来生产代理对象。

插入的代码就是切面代码。所以使用JDK动态代理可以实现AOP。

我们看个例子:

被代理对象实现的接口,只有接口中的方法才能够被代理:

public interface UserService {
public void addUser(User user);
public User getUser(int id);
}
public class UserServiceImpl implements UserService {
public void addUser(User user) {
System.out.println("add user into database.");
}
public User getUser(int id) {
User user = new User();
user.setId(id);
System.out.println("getUser from database.");
return user;
}
}

代理中间类:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class ProxyUtil implements InvocationHandler {
private Object target; // 被代理的对象
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("do sth before....");
Object result = method.invoke(target, args);
System.out.println("do sth after....");
return result;
}
ProxyUtil(Object target){
this.target = target;
}
public Object getTarget() {
return target;
}
public void setTarget(Object target) {
this.target = target;
}
}

测试:

import java.lang.reflect.Proxy;
import net.aazj.pojo.User;
public class ProxyTest {
public static void main(String[] args){
Object proxyedObject = new UserServiceImpl(); // 被代理的对象
ProxyUtil proxyUtils = new ProxyUtil(proxyedObject);
// 生成代理对象,对被代理对象的这些接口进行代理:UserServiceImpl.class.getInterfaces()
UserService proxyObject = (UserService) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
UserServiceImpl.class.getInterfaces(), proxyUtils);
proxyObject.getUser();
proxyObject.addUser(new User());
}
}

执行结果:

do sth before....
getUser from database.
do sth after....
do sth before....
add user into database.
do sth after....
我们看到在 UserService接口中的方法 addUser 和 getUser方法的前面插入了我们自己的代码。这就是JDK动态代理实现AOP的原理。

我们看到该方式有一个要求, 被代理的对象必须实现接口,而且只有接口中的方法才能被代理 。

2)CGLIB (code generate libary)

字节码生成技术实现AOP,其实就是继承被代理对象,然后Override需要被代理的方法,在覆盖该方法时,自然是可以插入我们自己的代码的。

因为需要Override被代理对象的方法,所以自然CGLIB技术实现AOP时,就 必须要求需要被代理的方法不能是final方法,因为final方法不能被子类覆盖 。

我们使用CGLIB实现上面的例子:

package net.aazj.aop;
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class CGProxy implements MethodInterceptor{
private Object target; // 被代理对象
public CGProxy(Object target){
this.target = target;
}
public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy proxy) throws Throwable {
System.out.println("do sth before....");
Object result = proxy.invokeSuper(arg0, arg2);
System.out.println("do sth after....");
return result;
}
public Object getProxyObject() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(this.target.getClass()); // 设置父类
// 设置回调
enhancer.setCallback(this); // 在调用父类方法时,回调 this.intercept()
// 创建代理对象
return enhancer.create();
}
}
public class CGProxyTest {
public static void main(String[] args){
Object proxyedObject = new UserServiceImpl(); // 被代理的对象
CGProxy cgProxy = new CGProxy(proxyedObject);
UserService proxyObject = (UserService) cgProxy.getProxyObject();
proxyObject.getUser();
proxyObject.addUser(new User());
}
}

输出结果:

do sth before....
getUser from database.
do sth after....
do sth before....
add user into database.
do sth after....
我们看到达到了同样的效果。
它的原理是生成一个父类 enhancer.setSuperclass( this.target.getClass()) 的子类 enhancer.create() ,然后对父类的方法进行拦截enhancer.setCallback( this) .
对父类的方法进行覆盖,所以父类方法不能是final的。

3) 接下来我们看下spring实现AOP的相关源码:

@SuppressWarnings("serial")
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface()) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}

从上面的源码我们可以看到:

if (targetClass.isInterface()) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);

如果被代理对象实现了接口,那么就使用JDK的动态代理技术,反之则使用CGLIB来实现AOP,所以 Spring默认是使用JDK的动态代理技术实现AOP的 。

JdkDynamicAopProxy的实现其实很简单:

final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {
@Override
public Object getProxy(ClassLoader classLoader) {
if (logger.isDebugEnabled()) {
logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());
}
Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised);
findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

3. Spring AOP的配置

Spring中AOP的配置一般有两种方法,一种是使用 <aop:config> 标签在xml中进行配置,一种是使用注解以及@Aspect风格的配置。

1) 基于<aop:config>的AOP配置

下面是一个典型的事务AOP的配置:

<tx:advice id="transactionAdvice" transaction-manager="transactionManager"?>
<tx:attributes >
<tx:method name="add*" propagation="REQUIRED" />
<tx:method name="append*" propagation="REQUIRED" />
<tx:method name="insert*" propagation="REQUIRED" />
<tx:method name="save*" propagation="REQUIRED" />
<tx:method name="update*" propagation="REQUIRED" />
<tx:method name="get*" propagation="SUPPORTS" />
<tx:method name="find*" propagation="SUPPORTS" />
<tx:method name="load*" propagation="SUPPORTS" />
<tx:method name="search*" propagation="SUPPORTS" />
<tx:method name="*" propagation="SUPPORTS" />
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="transactionPointcut" expression="execution(* net.aazj.service..*Impl.*(..))" />
<aop:advisor pointcut-ref="transactionPointcut" advice-ref="transactionAdvice" />
</aop:config>

再看一个例子:

<bean id="aspectBean" class="net.aazj.aop.DataSourceInterceptor"/>
<aop:config>
<aop:aspect id="dataSourceAspect" ref="aspectBean">
<aop:pointcut id="dataSourcePoint" expression="execution(public * net.aazj.service..*.getUser(..))" />
<aop:pointcut expression="" id=""/>
<aop:before method="before" pointcut-ref="dataSourcePoint"/>
<aop:after method=""/>
<aop:around method=""/>
</aop:aspect>
<aop:aspect></aop:aspect>
</aop:config>

<aop:aspect> 配置一个切面;

<aop:pointcut>配置一个切点,基于切点表达式;

<aop:before>,<aop:after>,<aop:around>是定义不同类型的advise. a

spectBean 是切面的处理bean:

public class DataSourceInterceptor {
public void before(JoinPoint jp) {
DataSourceTypeManager.set(DataSources.SLAVE);
}
}

2) 基于注解和@Aspect风格的AOP配置

我们以事务配置为例:首先我们启用基于注解的事务配置

<!-- 使用annotation定义事务 -->
<tx:annotation-driven transaction-manager="transactionManager" />

然后扫描Service包:

<context:component-scan base-package="net.aazj.service,net.aazj.aop" />

最后在service上进行注解:

@Service("userService")
@Transactional
public class UserServiceImpl implements UserService{
@Autowired
private UserMapper userMapper;
@Transactional (readOnly=true)
public User getUser(int userId) {
System.out.println("in UserServiceImpl getUser");
System.out.println(DataSourceTypeManager.get());
return userMapper.getUser(userId);
}
public void addUser(String username){
userMapper.addUser(username);
// int i = 1/0; // 测试事物的回滚
}
public void deleteUser(int id){
userMapper.deleteByPrimaryKey(id);
// int i = 1/0; // 测试事物的回滚
}
@Transactional (rollbackFor = BaseBusinessException.class)
public void addAndDeleteUser(String username, int id) throws BaseBusinessException{
userMapper.addUser(username);
this.m1();
userMapper.deleteByPrimaryKey(id);
}
private void m1() throws BaseBusinessException {
throw new BaseBusinessException("xxx");
}
public int insertUser(User user) {
return this.userMapper.insert(user);
}
}

搞定。这种事务配置方式,不需要我们书写pointcut表达式,而是我们在需要事务的类上进行注解。但是如果我们自己来写切面的代码时,还是要写pointcut表达式。下面看一个例子(自己写切面逻辑):

首先去扫描 @Aspect 注解定义的 切面:

<context:component-scan base-package="net.aazj.aop" />

切面代码:

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect // for aop
@Component // for auto scan
@Order() // execute before @Transactional
public class DataSourceInterceptor {
@Pointcut("execution(public * net.aazj.service..*.get*(..))")
public void dataSourceSlave(){};
@Before("dataSourceSlave()")
public void before(JoinPoint jp) {
DataSourceTypeManager.set(DataSources.SLAVE);
}
}

我们使用到了 @Aspect 来定义一个切面;

@Component是配合<context:component-scan/>,不然扫描不到;

@Order定义了该切面切入的顺序 ,因为在同一个切点,可能同时存在多个切面,那么在这多个切面之间就存在一个执行顺序的问题。

该例子是一个切换数据源的切面,那么他应该在 事务处理 切面之前执行,所以我们使用 @Order(0) 来确保先切换数据源,然后加入事务处理。

@Order的参数越小,优先级越高,默认的优先级最低:

/**
* Annotation that defines ordering. The value is optional, and represents order value
* as defined in the {@link Ordered} interface. Lower values have higher priority.
* The default value is {@code Ordered.LOWEST_PRECEDENCE}, indicating
* lowest priority (losing to any other specified order value).
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
public @interface Order {
/**
* The order value. Default is {@link Ordered#LOWEST_PRECEDENCE}.
* @see Ordered#getOrder()
*/
int value() default Ordered.LOWEST_PRECEDENCE;
}
关于数据源的切换可以参加专门的博文:http://www.cnblogs.com/digdeep/p/4512368.html

3) 切点表达式(pointcut)

上面我们看到,无论是 <aop:config> 风格的配置,还是 @Aspect 风格的配置,切点表达式都是重点。都是我们必须掌握的。

1>pointcut语法形式(execution):

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)

带有 ? 号的部分是可选的,所以可以简化成: ret-type-pattern name-pattern(param_pattern) 返回类型,方法名称,参数三部分来匹配 。

配置起来其实也很简单: * 表示任意返回类型,任意方法名,任意一个参数类型; .. 连续两个点表示0个或多个包路径,还有0个或多个参数 。就是这么简单。看下例子:

execution(* net.aazj.service..*.get*(..)) :表示net.aazj.service包或者子包下的以get开头的方法,参数可以是0个或者多个(参数不限);
execution(* net.aazj.service.AccountService.*(..)): 表示AccountService接口下的任何方法,参数不限;
注意这里,将类名和包路径是一起来处理的,并没有进行区分,因为类名也是包路径的一部分。
参数param- pattern 部分比较复杂: () 表示没有参数,(..)参数不限,(*,String) 第一个参数不限类型,第二参数为String .

2>within() 语法:

within()只能指定(限定)包路径(类名也可以看做是包路径),表示某个包下或者子报下的所有方法:

within(net.aazj.service.*), within(net.aazj.service..*),within(net.aazj.service.UserServiceImpl.*)

3>this() 与 target():

this是指代理对象,target是指被代理对象(目标对象)。所以 this() 和 target() 分别限定 代理对象的类型和被代理对象的类型:

this(net.aazj.service.UserService): 实现了UserService的代理对象(中的所有方法); 

target (net.aazj.service.UserService): 被代理对象 实现了UserService(中的所有方法);

4> args():

限定方法的参数的类型:

args(net.aazj.pojo.User): 参数为User类型的方法。
>@target(), @within(), @annotation(), @args():

这些语法形式都是针对注解的 ,比如 带有某个注解的 类 , 带有某个注解的 方法, 参数的类型 带有某个注解 :
@within(org.springframework.transaction.annotation.Transactional)
@target(org.springframework.transaction.annotation.Transactional)

两者都是指被代理对象 类 上有 @Transactional 注解的(类的所有方法),(两者似乎没有区别???)

@annotation(org.springframework.transaction.annotation.Transactional):  方法 带有 @Transactional 注解的所有方法 

@args(org.springframework.transaction.annotation.Transactional): 参数的类型 带有 @Transactional 注解 的所有方法 

6>bean(): 指定某个bean的名称

bean(userService): bean的id为 "userService" 的所有方法;

bean(*Service): bean的id为 "Service"字符串结尾的所有方法;

另外注意上面这些表达式是可以利用 ||, &&, ! 进行自由组合的。比如:execution(public * net.aazj.service..*.getUser(..)) && args(Integer,..)

4. 向注解处理方法传递参数

有时我们在写注解处理方法时,需要访问被拦截的方法的参数。此时我们可以使用 args() 来传递参数,下面看一个例子:

@Aspect
@Component // for auto scan
//@Order(2)
public class LogInterceptor {
@Pointcut("execution(public * net.aazj.service..*.getUser(..))")
public void myMethod(){};
@Before("myMethod()")
public void before() {
System.out.println("method start");
}
@After("myMethod()")
public void after() {
System.out.println("method after");
}
@AfterReturning("execution(public * net.aazj.mapper..*.*(..))")
public void AfterReturning() {
System.out.println("method AfterReturning");
}
@AfterThrowing("execution(public * net.aazj.mapper..*.*(..))")
// @Around("execution(public * net.aazj.mapper..*.*(..))")
public void AfterThrowing() {
System.out.println("method AfterThrowing");
}
@Around("execution(public * net.aazj.mapper..*.*(..))")
public Object Around(ProceedingJoinPoint jp) throws Throwable {
System.out.println("method Around");
SourceLocation sl = jp.getSourceLocation();
Object ret = jp.proceed();
System.out.println(jp.getTarget());
return ret;
}
@Before("execution(public * net.aazj.service..*.getUser(..)) && args(userId,..)")
public void before3(int userId) {
System.out.println("userId-----" + userId);
}
@Before("myMethod()")
public void before2(JoinPoint jp) {
Object[] args = jp.getArgs();
System.out.println("userId11111: " + (Integer)args[]);
System.out.println(jp.getTarget());
System.out.println(jp.getThis());
System.out.println(jp.getSignature());
System.out.println("method start");
}
}

方法:

@Before("execution(public * net.aazj.service..*.getUser(..)) && args(userId,..)")
public void before3(int userId) {
System.out.println("userId-----" + userId);
}

它会拦截 net.aazj.service 包下或者子包下的getUser方法,并且该方法的第一个参数必须是int型的, 那么使用切点表达式args(userId,..) 就可以使我们在切面中的处理方法before3中可以访问这个参数。

before2方法也让我们知道也可以通过 JoinPoint 参数来获得被拦截方法的参数数组。 JoinPoint 是每一个切面处理方法都具有的参数, @Around 类型的具有的参数类型为ProceedingJoinPoint。通过 JoinPoint或者 ProceedingJoinPoint 参数可以访问到被拦截对象的一些信息(参见上面的 before2 方法)。

面试题思考:解释一下什么叫AOP(面向切面编程)的更多相关文章

  1. Method Swizzling和AOP(面向切面编程)实践

    Method Swizzling和AOP(面向切面编程)实践 参考: http://www.cocoachina.com/ios/20150120/10959.html 上一篇介绍了 Objectiv ...

  2. Spring Boot2(六):使用Spring Boot整合AOP面向切面编程

    一.前言 众所周知,spring最核心的两个功能是aop和ioc,即面向切面和控制反转.本文会讲一讲SpringBoot如何使用AOP实现面向切面的过程原理. 二.何为aop ​ aop全称Aspec ...

  3. 浅谈Spring AOP 面向切面编程 最通俗易懂的画图理解AOP、AOP通知执行顺序~

    简介 我们都知道,Spring 框架作为后端主流框架之一,最有特点的三部分就是IOC控制反转.依赖注入.以及AOP切面.当然AOP作为一个Spring 的重要组成模块,当然IOC是不依赖于Spring ...

  4. 极简SpringBoot指南-Chapter05-SpringBoot中的AOP面向切面编程简介

    仓库地址 w4ngzhen/springboot-simple-guide: This is a project that guides SpringBoot users to get started ...

  5. AOP 面向切面编程, Attribute在项目中的应用

    一.AOP(面向切面编程)简介 在我们平时的开发中,我们一般都是面对对象编程,面向对象的特点是继承.多态和封装,我们的业务逻辑代码主要是写在这一个个的类中,但我们在实现业务的同时,难免也到多个重复的操 ...

  6. AOP面向切面编程的四种实现

     一.AOP(面向切面编程)的四种实现分别为最原始的经典AOP.代理工厂bean(ProxyFacteryBean)和默认自动代理DefaultAdvisorAutoProxyCreator以及Bea ...

  7. Javascript aop(面向切面编程)之around(环绕)

    Aop又叫面向切面编程,其中“通知”是切面的具体实现,分为before(前置通知).after(后置通知).around(环绕通知),用过spring的同学肯定对它非常熟悉,而在js中,AOP是一个被 ...

  8. [转] AOP面向切面编程

    AOP面向切面编程 AOP(Aspect-Oriented Programming,面向切面的编程),它是可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术. ...

  9. C# AOP 面向切面编程之 调用拦截

    有时候我们需要在代码中对方法调用进行拦截,并修改参数和返回值,这种操作叫做AOP(面向切面编程) 不过需要注意的是,AOP的效率很慢,在需要高效率场合慎用. 以下是C#的AOP方法: 首先建立一个控制 ...

  10. 【原创】Android AOP面向切面编程AspectJ

    一.背景: 在项目开发中,对 App 客户端重构后,发现用于统计用户行为的友盟统计代码和用户行为日志记录代码分散在各业务模块中,比如在视频模块,要想实现对用户对监控点的实时预览和远程回放行为进行统计, ...

随机推荐

  1. JAVA编译器常量

    编译器常量的特点就是:它的值在编译期就可以确定.比如: final int i = 5; 再傻的编译器也能在编译时看出它的值是5,不需要到运行时.对于运行时常量,它的值虽然在运行时初始化后不再发生变化 ...

  2. 转MQTT压力测试之Tsung的使用

    转自:http://www.cnblogs.com/lingyejun/p/7941271.html nTsung测试工具的基本测试命令为 Tsung -f  ~/.tsung/mqtt.xml -l ...

  3. Apache多虚拟主机多版本PHP(5.2+5.3+5.4)共存运行配置全过程

    因为某种需求,可能是因为早期的项目需要低版本的php,和目前开发所用的版本不太一致,我们需要给不同的虚拟主机配置不同版本的PHP.避免去额外配置多个Apache,等iis和apache共存的麻烦. 下 ...

  4. Oracle宣布很多其它的Java 9 新特性

    随着Oracle确认了其余的4个Java 9特性,下一代Java的计划開始变得更清晰了,Oracle已经发布了第二套Java 9特性.自从Oracle在今年早些时候宣布了3个新的API和模块化源代码后 ...

  5. Atitit.软件gui按钮and面板---os区-----linux windows搜索文件 文件夹

    Atitit.软件gui按钮and面板---os区-----搜索文件 1. Find 1 2. 寻找目录 1 3. 2. Locate// everything 1 4. 3. Whereis (wi ...

  6. 开源项目AndroidUtil-採用Fragment实现TabHost

    原文出自:方杰|http://fangjie.sinaapp.com/?p=141 转载请注明出处 学习Android也有一段时间了.感觉大部分的Android应用都有非常多类似的组件,所以就打算做了 ...

  7. 成为 Team Leader 后我最关心的那些事

    成为 Team Leader 后我最关心的那些事   推荐序 老有人问我 iOS 开发如何提高,今天收到一个来自网易的朋友投稿,分享他在成为 iOS 项目负责人之后面临的问题.文章中分享的如何招人,如 ...

  8. jQuery 效果 - slideToggle() 方法

    实例 通过使用滑动效果,在显示和隐藏状态之间切换 <p> 元素: $(".btn1").click(function(){ $("p").slide ...

  9. Linux Ubuntu 打开.exe文件

    这两天在编译Android源码,进行到要在Linux里安装烧录软件那一步,要先装驱动,故了解了如何在linux下打开.exe文件. .exe 文件在linux下不能直接打开,可有两种方式打开:. 1. ...

  10. TIM—基本定时器

    本章参考资料:< STM32F4xx 参考手册>.< STM32F4xx 规格书>.库帮助文档< stm32f4xx_dsp_stdperiph_lib_um.chm&g ...