AOP
3.1 动态代理
3.1.1实现方式
- JDK动态代理
- 使用JDK中的
Proxy , Method , InvocationHander 创建代理对象 - JDK动态代理要求目标类必须实现接口
- cglib动态代理
- 第三方的工具库,创建代理对象,原理是继承,通过继承目标类,创建子类,子类就是代理对象
- 要求目标类不能是final的,方法也不能是final的
3.1.2 作用
- 在目标类源码不改变的情况下,增加功能
- 减少代码重复
- 专注业务逻辑,没有干扰的日志或事务功能
- 解耦合,业务功能和日志,事务非业务功能分离
3.1.3 未使用AOP开发代码
? 目标:对业务类增加非业务方法
? 实现:先定义好接口与一个实现类,该实现类中除了要实现接口中的方法外,还要再写两个非业务方法。非业务方法也称为交叉业务逻辑。
定义业务接口:
public interface SomeService {
void doSome();
void doOther();
}
业务接口实现类:
public class SomeServiceImpl implements SomeService {
@Override
public void doSome() {
System.out.println("执行了业务方法doSome");
}
@Override
public void doOther() {
System.out.println("执行了业务方法doOther");
}
}
非业务方法:
? ? doTransaction():用于事务处理 ? ? doLog():用于日志处理 然后,再使接口方法调用它们。接口方法也称为主业务逻辑。
public void doLog(){
System.out.println("非业务方法, 日志功能,在方法开始时候,模拟输出日志");
}
public void doTrans(){
System.out.println("非业务方法, 事务功能,在方法结束后,模拟提交事务");
}
- 方式一:在业务实现类中,直接调用非业务方法
public class SomeServiceImpl2 implements SomeService {
@Override
public void doSome() {
doLog();
System.out.println("执行了业务方法doSome");
doTrans();
}
@Override
public void doOther() {
doLog();
System.out.println("执行了业务方法doOther");
doTrans();
}
public void doLog(){
System.out.println("非业务方法, 日志功能,在方法开始时候,模拟输出日志");
}
public void doTrans(){
System.out.println("非业务方法, 事务功能,在方法结束后,模拟提交事务");
}
}
- 方式二:将这些交叉业务逻辑代码放到专门的工具类或处理类中,由主业务逻辑调用。
ServiceTools代码:
public class ServiceTools {
public static void doLog(){
System.out.println("非业务方法, 日志功能,在方法开始时候,模拟输出日志");
}
public static void doTrans(){
System.out.println("非业务方法, 事务功能,在方法结束后,模拟提交事务");
}
}
实现类代码:
public class SomeServiceImpl3 implements SomeService {
@Override
public void doSome() {
ServiceTools.doLog();
System.out.println("执行了业务方法doSome");
ServiceTools.doTrans();
}
@Override
public void doOther() {
ServiceTools.doLog();
System.out.println("执行了业务方法doOther");
ServiceTools.doTrans();
}
}
- 方式三:动态代理
? 以上的解决方案,还是存在弊端:交叉业务与主业务深度耦合在一起。当交叉业务逻辑较多时,在主业务代码中会出现大量的交叉业务逻辑代码调用语句,大大影响了主业务逻辑的可读性,降低了代码的可维护性,同时也增加了开发难度。所以,可以采用动态代理方式。在不修改主业务逻辑的前提下,扩展和增强其功能
? MyInvocationHandler 继承 InvocationHandler 接口:
public class MyInvocationHandler implements InvocationHandler {
private Object target;
public MyInvocationHandler() {
super();
}
public MyInvocationHandler(Object target) {
super();
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object obj = null;
ServiceTools.doLog();
obj = method.invoke(target,args);
ServiceTools.doTrans();
return obj;
}
}
? 动态代理测试代码:
public class MyTest {
public static void main(String[] args) {
SomeServiceImpl target = new SomeServiceImpl();
MyInvocationHandler handler = new MyInvocationHandler(target);
SomeService proxy = (SomeService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
handler);
proxy.doSome();
System.out.println("===========================");
proxy.doOther();
}
}
3.2 AOP概述
3.2.1 AOP概念
AOP(Aspect Orient Programming)面向切面编程
- Aspect: 切面,给目标类增加的功能是切面,像上面用的日志,事务都是切面。切面的特点: 一般都是非业务方法,独立使用的。
- Orient:面向
- Programming:编程
3.2.2 AOP实现方式
? AOP:面向切面编程,基于动态代理,可以使用JDK,cglib两种代理方式。
? AOP就是动态代理的规范化,把动态代理对实现步骤和方式都定义好,开发人员用一种统一的方式,就是动态代理。
3.2.3 如何理解切面编程
? (1)需要在分析项目功能时,找出切面。
? (2)合理的安排切面的执行时间(在目标方法前, 还是目标方法后)
? (3)合理的安全切面执行的位置,在哪个类,哪个方法增加增强功能
3.2.4 面向切面编程对有什么好处?
- 减少重复
- 专注业务
注:面向切面编程知识面向对象的一种补充
3.3 AOP术语
3.3.1 AOP术语
(1)**切面(Aspect):**切面,表示增强的功能,非业务方法。泛指交叉业务逻辑,常见的事务处理、日志处理、统计信息、权限验证就可以理解为切面。
(2)**连接点(JoinPoint) :**连接点,连接业务方法和切面的位置,就是某个类中的业务方法。例如在动态代理中,doSome方法中加入了非业务方法,称该方法为连接点
(3)**切入点(Pointcut):**切入点,指多个连接点方法的集合。
(4)**目标对象:**给那个类的方法增加功能,这个类就是目标对象。
(5)**通知(Advice):**通知,通知表示切面功能执行的时间。通知定义了增强代码切入到目标代码的时间点,是目标方 法执行之前执行,还是之后执行等。
3.3.2 前面的三要素
一个切面有三个关键要素:
(1)切面的功能代码,切面干什么
(2)切面的执行位置,使用Pointcut表示切面的执行位置
(3)切面的执行时间,使用Advice表示时间,在目标方法之前还是之后
3.4 AOP的实现
? AOP是一个规范,是对动态代理的一个规范化,一个标注。
? AOP技术实现框架:
-
spring:spring在内部实现了AOP规范,能做AOP的工作。spring主要在事务处理时使用AOP。 日常项目开发中,很少使用spring的AOP实现,因为spring的AOP比较笨重。 -
aspectJ:一个开源专门做AOP的框架。spring框架中集成了aspectJ框架 aspectJ框架实现aop的两种方式: ? 1)使用xml的配置文件 ? 2)使用注解,我们在项目中使用aop功能,一般都使用注解,aspectJ一共有5个注解
3.5 学习 aspectJ的使用(基于注解)
3.5.1 切面的执行时间
? 切面的执行时间,在规范中叫做Advice(通知,增强)。在asepctJ框架中使用注解表示,也可以使用xml配置文件中的标签。在aspectJ中常用的物种类型:
(1)前置通知
(2)后置通知
(3)环绕通知
(4)异常通知
(5)最终通知
3.5.2 AspectJ 的切入点表达式
? 切面的位置,使用的是切入点表达式。表达式的原型是:
execution(modifiers-pattern? ret-type-pattern
declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
解释:
modifiers-pattern] 访问权限类型
ret-type-pattern 返回值类型
declaring-type-pattern 包名类名
name-pattern(param-pattern) 方法名(参数类型和参数个数)
throws-pattern 抛出异常类型
?表示可选的部分
? 简要表示为:
? execution(「访问权限」 方法返回值 方法声明(参数) 「异常类型」) =======>「」表示可选
切入点表达式要匹配的对象就是目标方法的方法名,所以,execution 表达式是方法的签名,可以使用以下符号简写:
? 举例:
execution(public * *(..)) :指定切入点为:任意公共方法。
execution(* set*(..)) :指定切入点为:任何一个以“set”开始的方法。
execution(* com.xxx.service.*.*(..)) : 指定切入点为:定义在 service 包里的任意类的任意方法。
execution(* com.xyz.service..*.*(..)) :指定切入点为:定义在 service 包或者子包里的任意类的任意方法。 “…”出现在类名中时,后面必须跟“*”,表示包、子包下的所有类。
execution(* *..service.*.*(..)) :指定所有包下的 serivce 子包下所有类(接口)中所有方法为切入点
3.5.3 使用AspectJ框架实现aop
3.5.3.1 使用步骤
- 新建maven项目
- 加入依赖:①spring依赖 ②aspect依赖
- 创建目标类:接口和实现类, 要做的就是给类中的方法增加功能
- 创建切面类:
- 在类的上面加入@Aspect
- 在类中定义方法,也就是切面要执行的功能代码。 在方法的上面加入aspectJ中的注解,例如:@Before 有需要指定切入点表达式execution()
- 创建spring配置文件:声明对象,把对象交给容器统一管理,声明对象可以使用注解或者xml配置文件的标签
- 声明目标类对象
- 声明切面类对象
- 声明aspectJ框架中的自动代理生成器标签
- 创建测试类,从spring容器中获取目标对象(实际就是代理对象),通过代理执行方法,实现aop功能增强。
举例:
(1)新建maven项目
(2)加入依赖:①spring依赖 ②aspect依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
(3)创建目标类:接口和实现类, 要做的就是给类中的方法增加功能
创建业务接口类SomeService :
public interface SomeService {
void doSome(String name, Integer age);
}
创建业务实现类SomeServiceImpl :
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(String name, Integer age) {
System.out.println("=====目标方法doSome()=====");
}
}
(4)创建切面类:
创建MyAspect类:
@Aspect
public class MyAspect {
@Before(value = "execution(public void com.st.ba01.SomeServiceImpl.doSome(String, Integer))")
public void myBefore(){
System.out.println("前置通知,切面功能,在目标方法之前输出时间" + new Date());
}
}
(5)创建spring配置文件:声明对象,把对象交给容器统一管理,声明对象可以使用注解或者xml配置文件的标签
- 声明目标类对象
- 声明切面类对象
- 声明aspectJ框架中的自动代理生成器标签
在resources目录下新建applicationContext.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.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="someService" class="com.st.ba01.SomeServiceImpl"/>
<bean id="myAspect" class="com.st.ba01.MyAspect"/>
<aop:aspectj-autoproxy/>
</beans>
(6)创建测试类,从spring容器中获取目标对象(实际就是代理对象),通过代理执行方法,实现aop功能增强。
public class MyTest {
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext app = new ClassPathXmlApplicationContext(config);
SomeService someService = (SomeService) app.getBean("someService");
System.out.println(someService.getClass().getName());
someService.doSome("李四", 20);
}
}
(7)输出结果:
3.5.3.2 注意事项
注意:
-
对于一个业务方法,可以配置多个切面 -
一个切面也可以配置到多个业务方法中 -
切面声明的@Before(value=“xxxx”),xxxx中如果写错,则目标类对象不会受影响 -
@Aspect:是aspectJ框架中的注解
- 作用:表示当前类是切面类
- 切面类:用来给业务方法增加功能的类,在这个类中有切面的功能代码
- 位置:在类定义的上面
-
方法的定义要求:
- 公共方法public
- 方法没有返回值
- 方法名称自定义
- 可以有参数 也可以没有;若有参数,参数不是自定义的,有几个参数类型可以使用
3.6 常用注解
3.6.1 @Before前置通知 - 方法有JoinPoint 参数
- 作用:在目标方法执行之前执行,被注解为前置通知的方法
- 语法:
@Before(value = "execution(public void com.st.ba01.SomeServiceImpl.doSome(String, Integer))") - 注解属性:
- 方法参数:
- JoinPoint jp【可选】:若使用则必须是第一个位置的参数,通过该参数,可获取切入点表达式、方法签名、目标对象等。
- 代码:
@Aspect
public class MyAspect {
@Before(value = "execution(public void com.st.ba01.SomeServiceImpl.doSome(String, Integer))")
public void myBefore4(JoinPoint jp){
System.out.println("方法的签名:" + jp.getSignature());
System.out.println("方法的名称:" + jp.getSignature().getName());
Object[] args = jp.getArgs();
for(Object arg:args){
System.out.println("参数包含:"+ arg);
}
System.out.println("前置通知,切面功能,在目标方法之前输出时间" + new Date());
}
}
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext app = new ClassPathXmlApplicationContext(config);
SomeService someService = (SomeService) app.getBean("someService");
someService.doSome("李四", 20);
}
运行结果:
?
### 3.6.2 @AfterReturning 后置通知 - 注解有 returning 属性
- 作用:在目标方法执行之后执行,被注解为后置通知的方法,由于是目标方法之后执行,所以可获取目标方法返回值。
- 语法:
@AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))", returning = "res") - 注解属性:
- value :切入点表达式
- returning:自定义的变量,表示目标方法的返回值,自定义变量名必须和通知方法的形参名一样
- 方法参数:
- JoinPoint 【可选】:若使用则必须是第一个位置的参数,通过该参数,可获取切入点表达式、方法签名、目标对象等。
- Object res【可选】:是目标方法执行后的返回值,根据返回值做切面的功能处理,推荐使用Object
- 代码:
MyAspect类:
```java
@AfterReturning(value = “execution(* *…SomeServiceImpl.doOther2(…))”, returning = “student”) public void myAfterReturningStudent(Object student){ System.out.println(“后置通知,获取的返回值是:”+student.toString()); Student student1 = (Student) student; student1.setName(“王五”); //对返回的对象 进行修改 } ```
SomeServiceImpl类:
public class SomeServiceImpl implements SomeService {
public Student doOther2() {
Student student = new Student();
student.setName("李四");
student.setAge(20);
return student;
}
}
@Test
public void test02(){
String config = "applicationContext.xml";
ApplicationContext app = new ClassPathXmlApplicationContext(config);
SomeService someService = (SomeService) app.getBean("someService");
Student student = someService.doOther2();
System.out.println(student.toString());
}
运行结果:
3.6.3 @Around 环绕通知-有 ProceedingJoinPoint 参数
- 作用:功能最强的通知,在目标方法执行之前之后执行,被注解为环绕增强的方法要有返回值。
- 语法:
@Around(value = "execution(* *..SomeServiceImpl.doFirst(..))") - 注解属性:
- 方法参数:
- ProceedingJoinPoint pjp【必选】:等同于Method,执行目标方法的。
pjp.proceed == method.invoke() - 方法返回值:Object,目标方法的执行结果,可以被修改
- 代码:
MyAspect类:
@Aspect
public class MyAspect {
@Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
public Object myAround2(ProceedingJoinPoint pjp) throws Throwable {
String name="";
Object[] args = pjp.getArgs();
if(args!=null && args.length > 1){
Object arg = args[0];
name = (String)arg;
}
Object result = null;
System.out.println("环绕通知:在目标方法之前,输出时间" + new Date());
if("张三".equals(name)){
result = pjp.proceed();
}
System.out.println("环绕通知:在目标方法之后,输出时间" + new Date());
if(result != null){
result = "新的返回值:hello aspectJ aop";
}
return result;
}
}
SomeServiceImpl类:
public class SomeServiceImpl implements SomeService {
@Override
public String doFirst(String name, Integer age) {
System.out.println("=====业务方法doFirst()=====");
return "doFirst";
}
}
@Test
public void test02(){
String config = "applicationContext.xml";
ApplicationContext app = new ClassPathXmlApplicationContext(config);
SomeService someService = (SomeService) app.getBean("someService");
String str = someService.doFirst("张三", 20);
System.out.println(str);
}
运行结果:
3.6.4 @AfterThrowing 异常通知-注解中有 throwing 属性
- 作用:在目标方法抛出异常后执行。可以做异常的监控程序,监控目标方法执行时是不是有异常,若存在异常,可以发送邮件,短信进行通知
- 语法:
@AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))", throwing = "ex") - 注解属性:
- value :切入点表达式
- throwing:自定义变量,表示目标方法抛出的异常对象,变量名必须和方法的参数名一样
- 方法参数:
- JoinPoint jp【可选】:若使用则必须是第一个位置的参数,通过该参数,可获取切入点表达式、方法签名、目标对象等。
- Throwable ex【必选】:参数名称为 throwing 指定的 名称,表示发生的异常对象。(Throwable 实现类即可)
- 方法返回值:void
- 代码:
MyAspect类:
@Aspect
public class MyAspect {
@AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))", throwing = "ex")
public void myAfterThrowing(Exception ex){
System.out.println("异常通知" + ex.getMessage());
}
}
SomeServiceImpl类,设置一个错误:
@Override
public void doSecond() {
System.out.println("业务方法doSecond" + 10/0);
}
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext app = new ClassPathXmlApplicationContext(config);
SomeService someService = (SomeService) app.getBean("someService");
someService.doSecond();
}
运行结果:
3.6.5 @After 最终通知
- 作用:无论目标方法是否抛出异常,该增强均会被执行,在目标方法之后执行的
- 语法:
@After(value = "execution(* *..SomeServiceImpl.doThird(..))") - 注解属性:
- 方法参数:
- JoinPoint jp【可选】:若使用则必须是第一个位置的参数,通过该参数,可获取切入点表达式、方法签名、目标对象等。
- 方法返回值:void
- 代码:
MyAspect类:
@Aspect
public class MyAspect {
@After(value = "execution(* *..SomeServiceImpl.doThird(..))")
public void myAfter(){
System.out.println("执行最终通知的");
}
}
SomeServiceImpl类:
@Override
public void doThird() {
System.out.println("业务方法doThird"+ 10/0);
}
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext app = new ClassPathXmlApplicationContext(config);
SomeService someService = (SomeService) app.getBean("someService");
someService.doThird();
}
运行结果:
3.6.6 @Pointcut 定义切入点
- 作用:当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护均较为麻烦。 AspectJ 提供了**@Pointcut 注解,用于定义 execution 切入点表达式**
- 语法:
@Pointcut(value = "execution(* *..SomeServiceImpl.doThird(..))") - 注解属性:
- 方法参数:无
- 方法返回值:void
- 代码:
MyAspect类:
@Aspect
public class MyAspect {
@After(value = "myPt()")
public void myAfter(){
System.out.println("执行最终通知的");
}
@Before(value = "myPt()")
public void myBefore(){
System.out.println("前置通知");
}
@Pointcut(value = "execution(* *..SomeServiceImpl.doThird(..))")
private void myPt(){
}
}
SomeServiceImpl类:
@Override
public void doThird() {
System.out.println("业务方法doThird");
}
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext app = new ClassPathXmlApplicationContext(config);
SomeService someService = (SomeService) app.getBean("someService");
someService.doThird();
}
运行结果:
|