一、什么是 AOP ?
1.1 AOP 解释
- AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
1.2 AOP 解决了什么问题?
- AOP (Aspect Oriented Programing) 及面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等待,Struts2的拦截器设计就是基于AOP的思想,是个比较经典的例子。
- 在不改变原有逻辑的基础上,给一些方法增加额外的功能,代理也是这个功能,读写分离也是用 aop 来做
1.3 AOP 技术的实现
- 一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;
- 二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。
二、AOP 的使用场景
使用 AOP 来横切关注点,具体可以在以下方面使用
- Authentication 权限
- Caching 缓存
- Context passing 内容传递
- Error handling 错误处理
- Lazy loading 懒加载
- Debugging 调试
- logging, tracing, profiling and monitoring 记录 跟踪 优化 校准
- Performance optimization 性能优化
- Persistence 持久化
- Resource pooling 资源池
- Synchronization 同步
- Transactions 事务
三、静态代理 ? 动态代理?
3.1 使用静态代理
使用 静态代理 是使用硬编码,方法相互嵌套的一种方案。
说到方法嵌套,相必大家应该都不陌生,我在开发 Java EE 应用程序的时候,有 dao 层负责和数据库打交道,service 层负责调用持久层的方法,然后会有 controller 调用 service 的方法并返回的结果返回给前台。
这样结合起来就很像套娃,接下来我们就用一个简单的实例来演示经验代理的实现
3.1.1 静态代理实例
- 编写一个名为 Clinet 的接口 ```java package cn.gorit.start;
public interface Client { public void say(); }
<a name="33OHe"></a>
###
2. 编写一个 People 类实现 Client 接口
```java
package cn.gorit.start;
public class People implements Client {
private String name;
public People(String name) {
this.name = name;
}
public void say() {
System.out.println(name+ "委托人说完了。。。");
}
}
- 编写 Lawer 类 ```java package cn.gorit.start;
public class Lawer { private Client wtr;
public Lawer(Client wtr) {
this.wtr = wtr;
System.out.println("我是律师,接下来该我说话了");
}
public void say() {
System.out.println("我是律师,我说完了,下面由我的委托人说话了");
wtr.say();
System.out.println("我是律师,我的委托人叙述完毕了");
}
}
4. 编写 Test 类
```java
package cn.gorit.start;
public class Test {
// 静态代理(硬编码) -- 方法的嵌套
public static void main(String[] args) {
Client wtr = new People("coco");
Lawer ls = new Lawer(wtr); //
ls.say();
}
}
代码的逻辑并不难,在 Lawer 类中 调用了 People 类中的 say 方法,实现了简单的静态代理
3.2 使用动态代理
动态代理的特点:
- 字节码随用随创建,随用随加载
作用
- 不修改源码的基础上对方法增强
动态代理的分类:
- 基于接口的动态代理
- 基于子类的动态代理
基于接口的动态代理:
- 涉及的类:Proxy
- 提供者:JDK 官方
如何创建代理对象?
- 使用 Proxy 类中的 newProxyInstance方法
创建代理对象的要求:_
- 被代理的类最少实现一个接口,如果没有则不能使用
这次我们以 JDK 自带的 Proxy 类实现动态代理
- 被代理的类至少实现一个接口,如果没有则不能使用
newProxyInstance方法的参数:
- ClassLoder:类加载器
- 它是用于加载代理对象字节码的,和被代理对象使用相同的的类加载器(固定写法)
- Class[]: 字节码数组
- 它是用于让代理独享和被代理对象有相同的方法,固定写法
- InvocationHandler:用于提供增强的代码
- 创建一个 IProducer 实现生产者 ```java package com.gorit.proxy;
/**
- 一个生产者
/ public interface IProducer { /*
- 销售
*/ public void saleProduct(float money);
/**
- 售后
- */ public void afterService(float money); } ```
- 创建 Producer 类实现 IProducer 接口 ```java package com.gorit.proxy;
/**
- 一个生产者
/ public class Producer implements IProducer{ /*
- 销售
*/ @Override public void saleProduct(float money) { System.out.println(“销售商品,并拿到钱:”+money); }
/**
- 售后
- */ @Override public void afterService(float money) { System.out.println(“提供售后服务,并拿到钱:”+money); } } ```
- 实现 Client 类,实现动态代理 ```java package com.gorit.proxy;
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy;
/**
- 模拟一个消费者 (基于接口的动态代理)
*/ public class Client { public static void main(String[] args) {
final Producer producer = new Producer(); IProducer proxyProducer = (IProducer) Proxy. newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler() { /** * 作用:执行被代理对象的任何接口方法都会经过该方法 * @param proxy 代理对象的引用 * @param method 当前执行的方法 * @param args 当前执行方法所需的参数 * @return 和代理对象方法有相同的返回值 * */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 提供增强的代码 Object returnValue = null; // 1. 获取方法执行的参数 Float money = (Float)args[0]; // 2. 判断当前方法是不是销售 if ("saleProduct".equals(method.getName())) { returnValue = method.invoke(producer,money*0.8f); } return returnValue; } }); proxyProducer.saleProduct(10000f);
} } ```
四、使用 Spring AOP (XML)
4.1 Spring 基于 XML 的 AOP 配置
- 把通知 Bean 也交给 spring 来管理
- 使用 aop:config 标签表名开始 AOP 配置
- 使用 aop:aspect 标签表名配置切面
- id 属性是给切面一个唯一标志
- ref 属性时指定通知类bean 的Id
- 在aop:aspect 标签内部使用对应的标签来配置通知的类型
- 我们现在示例是让 printLog 方法在切入点执行之前,所以是前置通知
- aop:before 表示配置前置通知
- method属性: 用于指定Logger 哪个方法是前置通知
- pointcut属性:用于指定切入点表达式,该表达式的含义是指对业务层中哪些方法增强
4.2 切入点表达式的写法:
- 关键字:execution(表达式)
- 表达式:访问修饰符 返回值 包名.包名.报名…方法名(参数列表)
- 标准表达式写法:
- public void cn.gorit.service.impl.AccountServiceImpl.saveAccount()
- 访问修饰符可以省略:
- void cn.gorit.service.impl.AccountServiceImpl.saveAccount()
- 返回值可以使用通配符,表示任意范沪指
- cn.gorit.service.impl.AccountServiceImpl.saveAccount()
- 报名可以用通配符表示任意包,但是有几级包就得写
- *. ….
- 全通配写法
- ...*(..)
- 实际开发中切入点表达式的通常写法:
- 切到业务层实现类下的所有方法:
- cn.gorit.service.impl..(..)
4.3 实例代码
本次实例以模拟日志打印进行 AOP 演示
pom.xml 依赖
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.3.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.7</version> </dependency> </dependencies>
- 编写工具类 Logger ,放在 util 包下 ```java package cn.gorit.utils;
import org.aspectj.lang.ProceedingJoinPoint;
/**
- 用于记录日志的工具类,它里面提供了公共的代码
*/ public class Logger {
/**
- 前置通知
*/ public void beforePrintlog() { System.out.println(“前置通知:Logger类找那个的 printlog 方法开始记录日志了”); }
/**
- 后置通知
- / public void afterReturninglog() { System.out.println(“后置通知:Logger类找那个的 afterReturnrintlog 方法开始记录日志了”); } /*
- 异常通知
- / public void afterThrowinglog() { System.out.println(“异常通知:Logger类找那个的 afterThrowingPrintlog 方法开始记录日志了”); } /*
- 最终通知
*/ public void lastPrintlog() { System.out.println(“最终通知:Logger类找那个的 lastPrintlog 方法开始记录日志了”); }
/**
- 环绕通知
- 问题:
- 当我们配置了环绕通知之后,切入点方法没哟执行,而通知方法执行了
- 分析:
- 通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码没有
- 解决:
- Spring框架为我们提供了一个接口: ProceedingJoinPoint。 该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。
- 该接口可以作为环绕通知的方法参数,在程序执行时,spring框架 会为我们提供该接口的实现类供我们使用。 *
- spring 中的环绕通知:
- 它是spring框架为我们提供的一种可以在代码中 手动控制增强方法何时执行的方式。
*/ public Object aroundPrintlog(ProceedingJoinPoint pjd) { Object rtValue = null; try {
Object[] args = pjd.getArgs();//得到方法执行所需要的参数 System.out.println("环绕通知:Logger类找那个的 aroundPrintlog 方法开始记录日志了 【前置】"); rtValue = pjd.proceed(args);//明确调用业务层方法 System.out.println("环绕通知:Logger类找那个的 aroundPrintlog 方法开始记录日志了 【后置】"); return rtValue;
} catch (Throwable t) {
System.out.println("环绕通知:Logger类找那个的 aroundPrintlog 方法开始记录日志了 【异常】"); throw new RuntimeException(t);
} finally {
System.out.println("环绕通知:Logger类找那个的 aroundPrintlog 方法开始记录日志了 【最终】");
} } } ```
- 编写 service 接口 IAccountService,模拟账户业务层接口 ```java package cn.gorit.service;
/**
- 账户的业务层接口
*/ public interface IAccountService {
/**
- 模拟保存账户
*/ public void saveAccount();
/**
- 模拟更新账户
*/ public void updateAccount(int i);
/**
- 删除账户
- */ public int deleteAccount(); }
3. 模拟账户的业务层实现类:AccountServiceImpl
```java
package cn.gorit.service.imp;
import cn.gorit.service.IAccountService;
/**
* 账户的业务层实现类
* */
public class AccountServiceImpl implements IAccountService {
public void saveAccount() {
System.out.println("执行了保存");
}
public void updateAccount(int i) {
System.out.println("执行了更新:"+i);
}
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
- 配置 bean.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">
<!-- Spring 的 IOC,把 service 配置进来 【要增强的方法】 -->
<bean id="accountService" class="cn.gorit.service.imp.AccountServiceImpl"></bean>
<!-- 配置Logger 类 -->
<bean id="logger" class="cn.gorit.utils.Logger"></bean>
<!-- 配置 AOP -->
<aop:config>
<!-- 配置切面 -->
<!--配置切入点表达式, id 属性用于指定表达式的唯一标识。 expression 属性 用于指定表达式内容
此标签写在 aop:aspect 标签内部职能当切面使用。
它还可以写在 aop:aspect 外面(必须写在上面),此时编程了所有切面可用-->
<aop:pointcut id="pt1" expression="execution( * cn.gorit.service.imp.*.*(..))"/>
<aop:aspect id="logAdvice" ref="logger">
<!-- <aop:before method="printlog" pointcut="execution(public void cn.gorit.service.imp.AccountServiceImpl.saveAccount())"></aop:before>-->
<!-- 配置前置通知:在切入点方法执行之前执行-->
<aop:before method="beforePrintlog" pointcut-ref="pt1"></aop:before>
<!-- 配置后置通知:在切入点方法正常执行之后-->
<aop:after-returning method="afterReturninglog" pointcut-ref="pt1"></aop:after-returning>
<!-- 配置异常通知-:在切入点方法产生异常之后执行-->
<aop:after-throwing method="afterThrowinglog" pointcut="execution( * cn.gorit.service.imp.*.*(..))"></aop:after-throwing>
<!-- 配置最终通知:无论切入点方法是否正常他都会在其后面执行-->
<aop:after method="lastPrintlog" pointcut-ref="pt1"></aop:after>
<!-- 配置环绕通知: 详细的注入请看 Logger类 -->
<aop:around method="aroundPrintlog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
</beans>
- 编写测试类 ```java package cn.gorit;
import cn.gorit.service.IAccountService; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test { public static void main(String[] args) { // 1. 获取容器 ApplicationContext ac = new ClassPathXmlApplicationContext(“bean.xml”); // 2. 获取对象 IAccountService as = (IAccountService) ac.getBean(“accountService”); // 3. 执行方法 as.saveAccount(); } }
![image.png](https://cdn.nlark.com/yuque/0/2020/png/473179/1591576983822-9e4ec8ff-29f0-4cf8-b940-47382da94556.png#align=left&display=inline&height=233&margin=%5Bobject%20Object%5D&name=image.png&originHeight=233&originWidth=601&size=119075&status=done&style=none&width=601)
<a name="RVdqn"></a>
# 五、使用 Spring AOP (注解)
<a name="e6IPG"></a>
### 5.1 配置文件
1. pom.xml
```xml
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
</dependencies>
- bean.xml 编写
```xml
<?xml version=”1.0” encoding=”UTF-8”?>
<!-- 配置 spring 创建容器时要扫描的包 (使用注解) -->
<context:component-scan base-package="cn.gorit"></context:component-scan>
<!--配置spring开启注解AOP 的支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<a name="YiMHu"></a>
### 5.2 JavaBean 编写
1. 编写 IAccountService 接口
```java
package cn.gorit.service;
/**
* 账户的业务层接口
* */
public interface IAccountService {
/**
* 模拟保存账户
* */
public void saveAccount();
/**
* 模拟更新账户
* */
public void updateAccount(int i);
/**
* 删除账户
* */
public int deleteAccount();
}
- 接口对应的实现类 AccountService ```java package cn.gorit.service.imp;
import cn.gorit.service.IAccountService; import org.springframework.stereotype.Service;
/**
- 账户的业务层实现类
- */ @Service(“AccountService”) public class AccountServiceImpl implements IAccountService {
public void saveAccount() {
System.out.println("执行了保存");
}
public void updateAccount(int i) {
System.out.println("执行了更新:"+i);
}
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
3. 工具类 Logger
```java
package cn.gorit.utils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* 用于记录日志的工具类,它里面提供了公共的代码
* */
@Component("logger")
@Aspect //表示当前类是一个切面类
public class Logger {
/**
* 表示切入点
*/
@Pointcut("execution( * cn.gorit.service.imp.*.*(..))")
private void pt1() {}
/**
* 前置通知
* */
@Before("pt1()")
public void beforePrintlog() {
System.out.println("前置通知:Logger类找那个的 printlog 方法开始记录日志了");
}
/**
* 后置通知
* */
@AfterReturning("pt1()")
public void afterReturninglog() {
System.out.println("后置通知:Logger类找那个的 afterReturnrintlog 方法开始记录日志了");
}
/**
* 异常通知
* */
@AfterThrowing("pt1()")
public void afterThrowinglog() {
System.out.println("异常通知:Logger类找那个的 afterThrowingPrintlog 方法开始记录日志了");
}
/**
* 最终通知
* */
@After("pt1()")
public void lastPrintlog() {
System.out.println("最终通知:Logger类找那个的 lastPrintlog 方法开始记录日志了");
}
/**
* 环绕通知
* 问题:
* 当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了
* 分析:
* 通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码没有
* 解决:
* Spring框架为我们提供了一个接口: ProceedingJoinPoint。 该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。
* 该接口可以作为环绕通知的方法参数,在程序执行时,spring框架 会为我们提供该接口的实现类供我们使用。
*
* spring 中的环绕通知:
* 它是spring框架为我们提供的一种可以在代码中 手动控制增强方法何时执行的方式。
* */
@Around("pt1()")
public Object aroundPrintlog(ProceedingJoinPoint pjd) {
Object rtValue = null;
try {
Object[] args = pjd.getArgs();//得到方法执行所需要的参数
System.out.println("环绕通知:Logger类找那个的 aroundPrintlog 方法开始记录日志了 【前置】");
rtValue = pjd.proceed(args);//明确调用业务层方法
System.out.println("环绕通知:Logger类找那个的 aroundPrintlog 方法开始记录日志了 【后置】");
return rtValue;
} catch (Throwable t) {
System.out.println("环绕通知:Logger类找那个的 aroundPrintlog 方法开始记录日志了 【异常】");
throw new RuntimeException(t);
} finally {
System.out.println("环绕通知:Logger类找那个的 aroundPrintlog 方法开始记录日志了 【最终】");
}
}
}
5.3 测试运行
package cn.gorit;
import cn.gorit.service.IAccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
// 1. 获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
// 2. 获取对象
IAccountService as = (IAccountService) ac.getBean("AccountService");
// 3. 执行方法
as.saveAccount();
}
}