AOP-面向切面编程
1、代理模式
定义:
为其他对象提供一种代理,以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
组成
- 抽象角色:通过接口或抽象类声明真实角色实现的业务方法。
- 代理角色:实现抽象角色,是真实角色的代理,通过调用真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作。
- 真实角色:实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用。
- 调用角色:想使用真实角色业务的角色,但实际是通过代理角色去调用。
1.1、静态代理
定义
静态代理是由程序员创建或工具生成代理类的源码,再编译代理类。所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了。
实现例子
以租房中介为例:房东要出租房子,客户要租房子,中介帮房东出租房子,客户只需要找中介租房子就可以了。基于这个例子,可以将四个角色做如下划分:
- 抽象角色:具有出租房子方法的接口
- 真实角色:房东,实现出租房子的接口
- 代理角色:中介,组合一个房东实例,实现出租房子的接口,接口中调用房东的出租方法并附加一些其他操作
- 调用角色:客户,依赖代理角色去调用其出租房子的方法来达到访问房东出租的方法
实现代码
-
抽象接口:出租接口
package com.zero.staticproxy; /** * 出租房子的接口 */ public interface Rent { /** * 出租房子的方法 */ void rent(); }
-
真实角色:房东
package com.zero.staticproxy; /** * 房东类 */ public class Landlord implements Rent{ /** * 实现出租的方法 */ public void rent() { System.out.println("房东出租房子"); } }
-
代理角色:中介
package com.zero.staticproxy; /** * 代理角色:中介 */ public class Proxy implements Rent { //组合一个房东对象 private Landlord landlord; //构造器注入 public Proxy(Landlord landlord){ this.landlord = landlord; } /** * 实现出租的接口 */ public void rent() { beforeRent(); //调用房东的出租方法 landlord.rent(); afterRent(); } /** * 附加操作1:出租前的方法 */ private void beforeRent(){ System.out.println("出租前调用的方法"); } /** * 附加操作2:出租后的方法 */ private void afterRent(){ System.out.println("出租后调用的方法"); } }
-
客户
package com.zero.staticproxy; /** * 客户类:通过中介调用房东的租房方法 */ public class Client { public static void main(String[] args) { //构造一个中介代理对象 Proxy proxy = new Proxy(new Landlord()); //通过代理对象去租房 proxy.rent(); } }
优点
- 真实角色的操作纯粹,只需关注自己的业务操作。
- 职责分离,将附加操作交给代理角色去做,实现了解耦。
缺点
- 每多一个真实角色,相应地就必须增加一个代理角色,代码量增加
1.2、动态代理
基于 jdk
的动态代理实现,其本质上还是满足代理模式的结构图,只是jdk
为我们提供了一个类和一个接口来动态生成代理类,以达到在执行时才生成代理类,而不是在编译时就已经写好代理类。其流程大概是这样子的:
思路:
- 真实对象实现抽象接口
InvocationHandlerImpl
实现jdk
提供的接口InvocationHandler
, 并组合一个真实对象,作为被代理对象Proxy
类通过依赖真实对象和InvocationHandlerImpl
的对象,动态构造代理角色- 调用角色通过调用代理角色的方法来实现调用真实对象的方法
小细节
- 在实现
InvocationHandler
的方法中,实现类会在实现方法中构造代理类与真实对象一致的方法接口。 - 由于需要给代理类构造与真实对象一致的接口方法,所以需要引入Proxy类去依赖真实对象,以获得该对象实现的接口信息和类加载器。(从该描述中可知,一个动态代理实例会对应一个
InvocationHandler
实例)
实现代码:以上面静态代理的例子进行重构
-
抽象角色
package com.zero.dynamic; /** * 出租房子的接口 */ public interface Rent { /** * 出租房子的方法 */ void rent(); }
-
真实角色
package com.zero.dynamic; /** * 房东类 */ public class Landlord implements Rent { /** * 实现出租的方法 */ public void rent() { System.out.println("房东出租房子"); } }
-
InvocationHandler
实现类package com.zero.dynamic; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; /** * 代理对象的程序调用处理类:实现程序调用处理接口 */ public class ProxyInvocationHandler implements InvocationHandler { //被代理的对象 private Rent rent; //set注入被代理的对象 public void setRent(Rent rent){ this.rent = rent; } /** * 通过该方法,代理对象能去调用真实对象的方法(类似于构建属于代理类的方法) * @param proxy * @param method * @param args * @return 调用真实对象方法的返回值 * @throws Throwable 方法的异常 */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //在执行真实角色方法前,调用打印日志的功能 log(method.getName()); //反射调用真实对象的方法 Object result = method.invoke(rent, args); //返回调用的结果 return result; } /** * 打印日志功能 * @param msg 日志消息 */ private void log(String msg){ System.out.println("[info] 调用了"+ msg + "方法..."); } }
-
客户角色
package com.zero.dynamic; import java.lang.reflect.Proxy; /** * 客户类:通过中介调用房东的租房方法 */ public class Client { public static void main(String[] args) { //真实角色 Landlord landlord = new Landlord(); //代理类的程序调用处理类 ProxyInvocationHandler invocationHandler = new ProxyInvocationHandler(); //给代理类的程序调用处理类注入被代理对象 invocationHandler.setRent(landlord); //通过Proxy类的静态方法创建动态代理类,参数1为要代理对象的类加载器,参数2为要代理对象实现的接口,参数3为与代理实例一一对应的InvocationHandler实例 Rent instance = (Rent) Proxy.newProxyInstance(landlord.getClass().getClassLoader(), landlord.getClass().getInterfaces(), invocationHandler); //调用代理类的方法,以间接访问真实对象 instance.rent(); } }
小结
基于jdk
的动态代理,依赖jdk
提供的接口InvocationHandler
和类Proxy
,通过实现提供的接口可以绑定代理实例要实现的方法(与真实对象一一对应的方法),通过Proxy
的静态方法newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
可以获得代理的实例,从而实现动态代理。
2、AOP
AOP在本质上,实际就是使用代理模式,在业务逻辑中加入附加的操作,但不改变原有业务代码的结构。让原有业务代码仍旧关注于自己的业务,而将附加的操作交予代理对象去做,然后再在需要附加操作的业务代码中将附加操作横切到业务代码之中。
2.1、基本概念介绍
AOP的一些概念
名词 | 官方描述 | 个人理解 |
---|---|---|
Aspect(切面) | 一个模块具有一组提供横切需求的 APIs。例如,一个日志模块为了记录日志将被 AOP 方面调用。应用程序可以拥有任意数量的方面,这取决于需求。 | 一个包含多个方法的类,该类提供附加操作用于横切进实际业务中 |
Join point(连接点) | 在你的应用程序中它代表一个点,你可以在插件 AOP 方面。 | 感觉和切入点很相似 |
Advice(通知) | 这是实际行动之前或之后执行的方法。这是在程序执行期间通过 Spring AOP 框架实际被调用的代码。 | 切面中的一个方法,代表一种附加操作 |
Pointcut | 这是一组一个或多个连接点,通知应该被执行。你可以使用表达式或模式指定切入点。 | 实际业务逻辑中的横切位置 |
Target object | 被一个或者多个方面所通知的对象,这个对象永远是一个被代理对象。也称为被通知对象。 | 相当于代理模式的目标对象 |
通知类型
通知 | 描述 |
---|---|
前置通知 | 在一个方法执行之前,执行通知。 |
后置通知 | 在一个方法执行之后,不考虑其结果,执行通知。 |
返回后通知 | 在一个方法执行之后,只有在方法成功完成时,才能执行通知。 |
抛出异常后通知 | 在一个方法执行之后,只有在方法退出抛出异常时,才能执行通知。 |
环绕通知 | 在方法调用之前和之后,执行通知。 |
2.2、spring中AOP的实现方式
在spring中,AOP的实现方式有三种,分别是:基于spring API接口来实现,通过配置文件来配置实现,以及通过注解来实现。虽然实现方式不同,但原理却也一样,但是基于动态代理方式。
基于spring API 的实现
- 引入
aspectjweaver.jar
以支持aop
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
<scope>runtime</scope>
</dependency>
- 实现spring提供的API接口(可选择性使用)
org.springframework.aop.MethodBeforeAdvice
接口
package com.zero.log;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.lang.Nullable;
import java.lang.reflect.Method;
/**
* 前置通知类:实现MethodBeforeAdvice接口,实现的方法会在切入点执行前被调用
*/
public class LogBefore implements MethodBeforeAdvice {
/**
* 这个方法和InvocationHandler的实现方法很相似,
* 但这个方法会在代理的方法执行前调用
* @param method 目标对象的方法
* @param args 目标对象的参数
* @param target 目标对象
* @throws Throwable
*/
public void before(Method method, Object[] args, @Nullable Object target) throws Throwable {
System.out.println(target.getClass().getName() + "类的" + method.getName() + "被执行了");
}
}
org.springframework.aop.AfterReturningAdvice
接口
package com.zero.log;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.lang.Nullable;
import java.lang.reflect.Method;
/**
* 后置带返回值的通知类:实现AfterReturningAdvice接口,实现的方法会在切入点执行结束后,带返回值被调用
*/
public class LogAfter implements AfterReturningAdvice {
/**
* 实现的方法
* @param returnValue 返回值
* @param method 目标方法
* @param args 参数
* @param target 目标对象
* @throws Throwable
*/
public void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable {
System.out.println(target.getClass().getName() + "类的" + method.getName() + "方法被调用了");
System.out.println("返回值为" + returnValue);
}
}
- 编写实际的业务接口和类
package com.zero.service;
/**
* Userservice接口,用于模拟业务接口
*/
public interface UserService {
void add();
void delete();
void update();
void select();
}
package com.zero.service;
/**
* UserServiceImpl, 模拟业务接口的实现类
*/
public class UserServiceImpl implements UserService {
public void add() {
System.out.println("添加一个用户");
}
public void delete() {
System.out.println("删除一个用户");
}
public void update() {
System.out.println("更新一个用户");
}
public void select() {
System.out.println("查询一个用户");
}
}
- 在配置文件中进行AOP配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 首先注入bean对象,将实现API的类和业务类配置进来 -->
<bean id="userService" class="com.zero.service.UserServiceImpl"/>
<bean id="logBefore" class="com.zero.log.LogBefore"/>
<bean id="logAfter" class="com.zero.log.LogAfter"/>
<!-- 其次,通过aop:config标签配置aop -->
<aop:config>
<!--
aop:pointcut代表一个切入点, id为切入点的唯一标识,
expression表示切入点的位置,括号内为一种表达式,默认可为(* *.*(..))
第一个* 表示返回值,第二个表示类名,第三个表示方法名,(..)表示方法参数
-->
<aop:pointcut id="userServicePointCut" expression="execution(* com.zero.service.UserServiceImpl.*(..))"/>
<!-- aop:advisor表示定义一个通知,advice-ref表示引用一个通知类(比如上面实现API接口的bean对象)
pointcut-ref表示通知的切入点引用
-->
<aop:advisor advice-ref="logBefore" pointcut-ref="userServicePointCut"/>
<aop:advisor advice-ref="logAfter" pointcut-ref="userServicePointCut"/>
</aop:config>
</beans>
- 测试
import com.zero.service.UserService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MyTest {
@Test
public void test(){
//获取容器
ApplicationContext context = new ClassPathXmlApplicationContext("applicationConfig.xml");
//获取bean对象
UserService userService = context.getBean("userService", UserService.class);
//执行对象方法
userService.add();
System.out.println("----------------------------");
//执行对象方法
userService.delete();
System.out.println("----------------------------");
//执行对象方法
userService.update();
System.out.println("----------------------------");
//执行对象方法
userService.select();
}
}
- 结果
com.zero.service.UserServiceImpl类的add被执行了
添加一个用户
com.zero.service.UserServiceImpl类的add方法被调用了
返回值为null
----------------------------
com.zero.service.UserServiceImpl类的delete被执行了
删除一个用户
com.zero.service.UserServiceImpl类的delete方法被调用了
返回值为null
----------------------------
com.zero.service.UserServiceImpl类的update被执行了
更新一个用户
com.zero.service.UserServiceImpl类的update方法被调用了
返回值为null
----------------------------
com.zero.service.UserServiceImpl类的select被执行了
查询一个用户
com.zero.service.UserServiceImpl类的select方法被调用了
返回值为null
-
小结
实现API接口的通知类,其通知成功地切入到切入点的指定位置进行附加操作。
2.3、通过配置文件来配置实现
-
编写切面(一个附加操作的类,与普通类没什么两样)
package com.zero.log; import org.aspectj.lang.ProceedingJoinPoint; /** * 切面类,和普通类没什么两样 */ public class Log { //用于前置通知 void before(){ System.out.println("方法执行前--------"); } //用于做后置通知 void after(){ System.out.println("方法执行后"); } //用于环绕通知 //ProceedingJoinPoint类的对象参数,可以访问连接点(即横切的方法) void around(ProceedingJoinPoint jp) throws Throwable { //方法执行前通知 System.out.println("环绕前-----"); //调用ProceedingJoinPoint对象的proceed()方法可以调用横切的方法,类似于过滤器过滤的操作 jp.proceed(); //方法执行后通知 System.out.println("环绕后-----"); } //用于返回后通知 //参数为返回结果 void afterReturn(Object result){ System.out.println("方法返回后,返回值:" + result); } //用于异常后通知 //参数为异常 void afterExcption(Throwable ex){ System.out.println("出现错误后, 异常为:" + ex); } }
-
编写业务接口和类(此处采用2.2中的业务接口和类)
-
将切面类和业务类的bean对象配置到容器中
<!-- 首先注入bean对象,将实现API的类和业务类配置进来 --> <bean id="userService" class="com.zero.service.UserServiceImpl"/> <bean id="log" class="com.zero.log.Log"/>
-
配置切面
<!-- 使用aop:config标签进行配置aop --> <aop:config> <!-- 使用aop:aspect标签进行切面配置,id表示切面的唯一标识,ref表示引用的切面类对象 --> <aop:aspect id="logAspect" ref="log"> <!-- aop:pointcut标签定义一个切点 --> <aop:pointcut id="userServicePointCut" expression="execution(* com.zero.service.UserServiceImpl.*(..))"/> <!-- aop:before表示前置通知,method是切面里的方法名,pointcut-ref表示引用切点 --> <aop:before method="before" pointcut-ref="userServicePointCut"/> <aop:after method="after" pointcut-ref="userServicePointCut"/> <!-- returning表示方法的返回值参数,和切面方法中的参数对应 --> <aop:after-returning method="afterReturn" returning="result" pointcut-ref="userServicePointCut"/> <aop:around method="around" pointcut-ref="userServicePointCut" /> <!-- throwing表示方法的异常结果的参数,和切面方法中的参数对应 --> <aop:after-throwing method="afterExcption" throwing="ex" pointcut-ref="userServicePointCut"/> </aop:aspect> </aop:config>
-
测试
@Test public void test1(){ //获取容器 ApplicationContext context = new ClassPathXmlApplicationContext("applicationConfig1.xml"); //获取bean对象 UserService userService = context.getBean("userService", UserService.class); //执行对象方法 userService.add(); System.out.println("----------------------------"); //执行对象方法 userService.delete(); System.out.println("----------------------------"); //执行对象方法 userService.update(); System.out.println("----------------------------"); //执行对象方法 userService.select(); }
结果:
方法执行前-------- 环绕前----- 添加一个用户 环绕后----- 方法返回后,返回值:null 方法执行后 ---------------------------- 方法执行前-------- 环绕前----- 删除一个用户 环绕后----- 方法返回后,返回值:null 方法执行后 ---------------------------- 方法执行前-------- 环绕前----- 更新一个用户 环绕后----- 方法返回后,返回值:null 方法执行后 ---------------------------- 方法执行前-------- 环绕前----- 查询一个用户 环绕后----- 方法返回后,返回值:null 方法执行后
2.4、通过注解来实现
-
开启
aop
织入支持<!-- 开启aop代理支持 --> <aop:aspectj-autoproxy />
-
编写切面类
package com.zero.log; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; //使用@Aspect注解将该类注解为一个切面 @Aspect public class LogAnnotation { //使用@Pointcut注解将方法注解为切入点,注解参数为切入点表达式,需要使用切入点时,调用方法即可 @Pointcut("execution(* com.zero.service.*.*(..))") public void userServicePointcut(){} //使用@Before注解将该方法注解为一个前置通知,注解的参数为横切点 @Before("userServicePointcut()") void before(){ System.out.println("方法执行前--------"); } //使用@After注解将该方法注解为一个后置通知,注解的参数为横切点 @After("userServicePointcut()") void after(){ System.out.println("方法执行后"); } //使用@Around注解将该方法注解为一个环绕通知,注解的参数为横切点 @Around("userServicePointcut()") //ProceedingJoinPoint类的对象参数,可以访问连接点(即横切的方法) void around(ProceedingJoinPoint jp) throws Throwable { //方法执行前通知 System.out.println("环绕前-----"); //调用ProceedingJoinPoint对象的proceed()方法可以调用横切的方法,类似于过滤器过滤的操作 jp.proceed(); //方法执行后通知 System.out.println("环绕后-----"); } //使用@AfterReturning注解将该方法注解为一个后置返回通知,注解的参数为横切点和返回值 @AfterReturning(pointcut = "userServicePointcut()", returning = "result") void afterReturn( Object result ){ System.out.println("方法返回后,返回值为:" + result); } //使用@AfterThrowing注解将该方法注解为一个后置异常通知,注解的参数为横切点和异常参数 @AfterThrowing(pointcut = "userServicePointcut()", throwing = "ex") void afterExcption(Throwable ex){ System.out.println("出现错误后"); } }
-
编写业务接口和类
使用2.1中的业务接口和类
-
将切面类和业务类的bean对象托管给spring容器
<!-- 首先注入bean对象,将实现API的类和业务类配置进来 --> <bean id="userService" class="com.zero.service.UserServiceImpl"/> <bean id="log" class="com.zero.log.LogAnnotation"/>
-
测试
@Test public void test2(){ //获取容器 ApplicationContext context = new ClassPathXmlApplicationContext("applicationConfig2.xml"); //获取bean对象 UserService userService = context.getBean("userService", UserService.class); //执行对象方法 userService.add(); System.out.println("----------------------------"); //执行对象方法 userService.delete(); System.out.println("----------------------------"); //执行对象方法 userService.update(); System.out.println("----------------------------"); //执行对象方法 userService.select(); }
结果
环绕前----- 方法执行前-------- 添加一个用户 环绕后----- 方法执行后 方法返回后,返回值为:null ---------------------------- 环绕前----- 方法执行前-------- 删除一个用户 环绕后----- 方法执行后 方法返回后,返回值为:null ---------------------------- 环绕前----- 方法执行前-------- 更新一个用户 环绕后----- 方法执行后 方法返回后,返回值为:null ---------------------------- 环绕前----- 方法执行前-------- 查询一个用户 环绕后----- 方法执行后 方法返回后,返回值为:null
2.5、小结
三种方式的总结:
- 第一种方式,通过实现Spring提供的API接口,将类直接作为一个通知类进行编写(实现接口的方法),然后再在配置容器中通过
aop:advisor
标签配置通知的切入点就可以了 - 第二种方式,采用配置文件的方式进行配置,可以自定义一个切面类,然后通过
aop:aspect
标签配置该类为一个切面类,再通过aop:before
,aop:after
,aop:around
,aop:after-returning
,aop:after-throwing
等标签将切面类的方法配置为一个个对应的通知。 - 第三种方式,采用注解的方式,在自定义一个切面类时,就使用注解将类注解为配置类,将方法注解为通知。
配置文件和注解方式的一一对应
配置文件的每个标签和注解中的每个注解,其功能是存在一一对应的,如下:
配置文件标签 | 注解名称 | 作用 |
---|---|---|
<aop:aspect> | @Aspect | 将类定义为切面类 |
<aop:before> | @Before | 将方法定义为前置通知 |
<aop:after> | @After | 将方法定义为后置通知 |
<aop:around> | @Around | 将方法定义为环绕通知 |
<aop:after-returning> | @AfterReturning | 将方法定义为返回后通知 |
<aop:after-throwing> | @AfterThrowing | 将方法定义为异常后通知 |
<aop:pointcut> | @Pointcut | 定义一个切入点 |
小细节
- 使用
aop
功能需要导入aspectjrt.jar
,aspectjweaver.jar
,aspectj.jar
,aopalliance.jar
, 但是导入aspectjweaver.jar
时会自动导入其他jar包。 - 使用
aop
功能时必须引入aop
的命名约束 - 使用注解配置
aop
时,需要在配置文件中开启aop
织入支持,当然使用完整注解来代替配置文件时可以忽略 - 在idea中想看
aop
配置相关的属性,可以使用打空格来查看;同理对于注解的属性可以通过查看源码注释来查看可以配置哪些参数,以及参数功能。
新手上路,恐有错漏,望客官看看即可,还得参照各路大神博文,以免被我误入歧途。