SpringAOP
在学习编程过程中,我们对于公共方法的处理应该是这样的一个过程,初期阶段如下
f1(){Date now = new Date();System.out.println("功能执行之前的各种前置工作"+now)//...功能代码//...功能代码System.out.println("功能执行之前的各种后置工作") } f2(){System.out.println("功能执行之前的各种前置工作")//...功能代码//...功能代码System.out.println("功能执行之前的各种后置工作") } .....
然后中期阶段,我们学会了封装
public class AspectConfig{static void before(){System.out.println("功能执行之前的各种前置工作")}static void after(){System.out.println("功能执行之前的各种后置工作")} }
AspectConfig.before() f1() AspectConfig.after()AspectConfig.before()---切面-- 块 -份--模块 f2() AspectConfig.after()
现在,我们可以采用另外一种更简单的方式,AOP思想,对代码进行无侵入式实现,这样更高级了。
public class AspectConfig{@express(*f*)static void before(){System.out.println("功能执行之前的各种前置工作")}//@expres(*f*)static void after(){System.out.println("功能执行之前的各种后置工作")} }
m(){f1() } n(){f2() }
1.1 AOP思想概述
1.1.1 思想简介
AOP(Aspect Oriented Programming)意为:面向切面编程,它是一种思想,是对某⼀类事情的集中处理。
它的核心思想是 将方法在执行过程中切分为多个部分,也就是多个横切关注点,这样可以与业务逻辑代码分离开来,使代码模块化,进而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
AOP是OOP(面向对象编程)的一个补充,它允许开发者以声明方式实现关注点,而不是通过在业务逻辑代码中散布大量重复代码。
1.1.2 AOP的实现方式
AOP 可以通过多种方式实现,包括:
-
编译时增强:在编译期间通过修改字节码来实现AOP。
-
类加载时增强:在类加载到JVM时通过字节码操作实现AOP。
-
动态代理:在程序运行时,通过代理对象来实现AOP。
1.1.3 AOP的应用场景
1)日志记录
2)声明式事务管理
3)登录安全验证
4)统一异常处理
1.1.3 AOP中的核心概念
1)横切关注点
在程序中,可以跨越多个模块的方法或功能。即是,与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志 , 安全 , 缓存 , 事务等等 … 简单来说,就是程序员要关注的事情,抽象概念
2)切面(Aspect)
横切关注点被模块化的一个体现,它是一个类。这个类由通知(Advice)和切点(Pointcut)组成,既包含了横切逻辑的定义,也包含了连接点的定义。
3)通知(Advice)
通知,实际上是一个拦截器,它定义了切面是做什么以及何时使用,即在某个特定的连接点上执行的动作,它是切面的具体实现
以目标方法为参照点,根据放置位置的地方不同,通知分为如下5种类型通知:
-
前置通知(Before):在方法执行前执行。
-
后置通知(After):在方法执行后执行。
-
返回通知(After Returning):在方法成功返回后执行。
-
异常通知(After Throwing):在方法抛出异常后执行。
-
环绕通知(Around):包围方法执行的前后。
4)切入点(PointCut)
Advice 执行的 “地点”的定义。 简单理解:就是用来定义位置
5)连接点(JointPoint)
与切入点匹配的执行点。这个点可以是方法调用时,抛出异常时,甚至修改字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。 简单理解:就是具体切入的位置
6)目标(Target)
就是被切面作用的对象,包含连接点的对象
1.2 Spring AOP简介
AOP是一个思想,是一个编程范式。SpringAOP是对这个思想的实现。
Spring AOP 是构建在动态代理基础上的。因此 Spring 对 AOP 的⽀持局限于⽅法级别的拦截。 Spring AOP ⽀持 JDK Proxy
和 CGLIB
⽅式实现动态代理。默认情况下,实现了接⼝的类,使⽤ AOP 会基于 JDK ⽣成代理类,没有实现接⼝的类,会基于 CGLIB ⽣成代理类。
织⼊(Weaving):代理的⽣成时机
织入就是什么时候把代理的代码放进运行的代码中。在⽬标对象的⽣命周期⾥有多个点可以进⾏织⼊:
编译期(编译阶段):
切⾯在⽬标类编译时被织⼊。这种⽅式需要特殊的编译器。AspectJ的织⼊编译器就 是以这种⽅式织⼊切⾯的。
类加载期:
切⾯在⽬标类加载到JVM时被织⼊。这种⽅式需要特殊的类加载器 (ClassLoader),它可以在⽬标类被引⼊应⽤之前增强该⽬标类的字节码。AspectJ5的加载 时织⼊(load-time weaving. LTW)就⽀持以这种⽅式织⼊切⾯。
运⾏期:
切⾯在应⽤运⾏的某⼀时刻被织⼊。⼀般情况下,在织⼊切⾯时,AOP容器会为⽬标对象动态创建⼀个代理对象。SpringAOP就是以这种⽅式织⼊切⾯的。
1.3 XML配置方式
1.3.1 导入依赖
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.4</version> </dependency>
1.3.2 案例1
UserService.java
public interface UserService {void add();void delete();void update();void search(); }
UserServiceImpl.java
public class UserServiceImpl implements UserService{@Overridepublic void add() {System.out.println("---增加用户---");} @Overridepublic void delete() {System.out.println("---删除用户---");} @Overridepublic void update() {System.out.println("---修改用户---");} @Overridepublic void search() {System.out.println("---查询用户---");} }
编写两个通知类型,一个前置通知,一个后置通知
import org.springframework.aop.MethodBeforeAdvice; import java.lang.reflect.Method; public class Log implements MethodBeforeAdvice {/**** @param method 目标对象的方法* @param objects 被调用的方法的参数* @param o 目标对象* @throws Throwable*/@Overridepublic void before(Method method, Object[] objects, Object o) throws Throwable {System.out.println(o.getClass().getName()+"的"+method.getName()+"正在执行····");} }
import org.springframework.aop.AfterReturningAdvice; import java.lang.reflect.Method; public class AfterLog implements AfterReturningAdvice {/**** @param o 方法的返回值* @param method 目标对象的方法* @param objects 目标对象的方法的参数* @param o1 目标对象* @throws Throwable*/@Overridepublic void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {System.out.println("执行了"+o1.getClass().getName()+"的"+method.getName()+"方法," +"返回值:"+o);} }
注册bean,实现aop切入 , 注意导入约束
<?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:context="http://www.springframework.org/schema/context"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!--注册--><bean id="userService" class="com.shuilidianli.service.UserServiceImpl"></bean><bean id="log" class="com.shuilidianli.aop.Log"></bean><bean id="afterLog" class="com.shuilidianli.aop.AfterLog"></bean> <!--aop配置--><aop:config><!--切入点 expression: 表达式匹配要执行的方法--><aop:pointcut id="pointcut" expression="execution(* com.shuilidianli.service.UserServiceImpl.*(..))"/><!--执行环绕:advice-ref执行方法,pointcut-ref:切入点--><aop:advisor advice-ref="log" pointcut-ref="pointcut"/><aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/></aop:config> </beans>
测试
import com.shuilidianli.service.UserService; import com.shuilidianli.web.EmpController; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class AopTest2 {@Testpublic void test1(){ApplicationContext ctx =new ClassPathXmlApplicationContext("beans.xml");UserService us =(UserService) ctx.getBean("userService");us.search(); } }
测试结果:
Aop的重要性 : 很重要 . 一定要理解其中的思路 , 主要是思想的理解这一块 .
Spring的Aop就是将公共的业务 (日志 , 安全等) 和领域业务结合起来 , 当执行领域业务时 , 将会把公共业务加进来 . 实现公共业务的重复利用 . 领域业务更纯粹 , 程序员专注领域业务 , 其本质还是动态代理
1.3.2 案例2
自定义类来实现Aop
目标业务类不变依旧是userServiceImpl
1)自己定义一个切入类
package com.shuilidianli.aop; public class DiyPointcut {public void before(){System.out.println("---------方法执行前---------");}public void after(){System.out.println("---------方法执行后---------");} }
2)注册配置
<!--第二种方式自定义实现--> <!--注册bean--> <bean id="diy" class="com.shuilidianli.aop.DiyPointcut"></bean> <!--aop的配置--> <aop:config><!--第二种方式:使用AOP的标签实现--><aop:aspect ref="diy"><aop:pointcut id="diyPointcut" expression="execution(* com.shuilidianli.service.UserServiceImpl.*(..))"/><aop:before method="before" pointcut-ref="diyPointcut"/><aop:after method="after" pointcut-ref="diyPointcut"/></aop:aspect> </aop:config>
3)测试
package com.shuilidianli.test; import com.shuilidianli.service.UserService; import com.shuilidianli.web.EmpController; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class AopTest2 {@Testpublic void test1(){ApplicationContext ctx =new ClassPathXmlApplicationContext("beans.xml");UserService us =(UserService) ctx.getBean("userService");us.search(); } }
测试结果:
1.4 注解方式
1.4.1 导入依赖,开启注解扫描
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.4</version> </dependency>
<context:component-scan base-package="com"/> <!-- 开启aop组件注解扫描 --> <aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
aop:aspectj-autoproxy:说明
通过aop命名空间的<aop:aspectj-autoproxy />声明自动为spring容器中那些配置@aspect切面的bean创建代理,织入切面。 当然,spring 在内部依旧采用AnnotationAwareAspectJAutoProxyCreator进行自动代理的创建工作,但具体实现的细节已经被<aop:aspectj-autoproxy />隐藏起来了 <aop:aspectj-autoproxy />有一个proxy-target-class属性,默认为false,表示使用jdk动态代理织入增强, 当配为<aop:aspectj-autoproxy poxy-target-class="true"/>时,表示使用CGLib动态代理技术织入增强。 不过即使proxy-target-class设置为false,如果目标类没有声明接口,则spring将自动使用CGLib动态代理。
1.4.2 切入点表达式语法
1)语法如下:
"execution(修饰词 返回值类型 类全限定名.方法名(形参列表) 异常)"public int f1(int a,int b)throw Exception{}
2)Spring AOP的切入点表达式非常灵活,支持模糊配置。
eg1 : execution(* 全类名.*(..))第一个 "*" 表示支持任意修饰符及返回值类型;第二个 "*" 表示支持该类中的任意方法;形参列表中的".."则表示可以匹配任意数量和类型的参数。(PS : 若目标类、接口与当前切面类在同一个包下,可以省略包名,只写类名) eg2 : execution(public * 全类名.*(..))表示支持该类中的所有公有的方法 eg3 : execution(public double 全类名.*(..))表示支持该类中所有公有的且返回值为double的方法 eg4 : execution(public double 全类名.*(double, ..))表示支持该类中所有形参列表第一个参数为double类型,且后续参数可以是任意数量任意类型的,公有的返回值为double的方法。 eg5 : execution(double 全类名.*(double, double)表示支持该类中所有形参列表为两个double类型,公有的且返回值为double类型的方法。
3) 在AspectJ(另一个框架)中,切入点表达式可以通过"&&","||","!"等操作符结合起来。
eg : execution(* *.add(int, ..)) || execution(* *.subtract(int, ..))——表示支持任意类中的任意访问修饰符和任意返回值类型的,且形参列表第一个参数为int类型的add 或 subtract方法。
4)注意事项:
(1) 当切入点表达式直接指向了接口某个实现类的方法(非实现类特有方法),这时切入点表达式仅会对该实现类生效(动态代理 + 反射),即接口的其他实现类不会生效(不会得到代理对象,即使你以接口类型作为接收)。 (2) 当切入点表达式指向接口的方法时,切入表达式会对该接口的所有实现类生效。 (3) 切入点表达式也可以切入到没有实现接口的类的横切关注点中。(CGlib动态代理模式) PS : JDK Proxy动态代理和CGlib动态代理的区别 - JDK动态代理是面向接口的,只能增强实现类中重写了接口中的方法。而CGlib是面向父类的,可以增强父类的所有方法。 - JDK得到的对象是JDK代理对象实例,而CGlib得到的对象是被代理对象的子类。
1.4.3 案例3
AnnotationPointcut.java
package com.shuilidianli.aop; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Component @Aspect public class AnnotationPointcut {@Before("execution(* com.shuilidianli.service.UserServiceImpl.*(..))")public void before(){System.out.println("---------方法执行前---------");}@After("execution(* com.shuilidianli.service.UserServiceImpl.*(..))")public void after(){System.out.println("---------方法执行后---------");}@Around("execution(* com.shuilidianli.service.UserServiceImpl.*(..))")public void around(ProceedingJoinPoint joinPoint) throws Throwable {System.out.println("---环绕前---");System.out.println("调用的方法:"+joinPoint.getSignature());//执行目标方法Object proceed = joinPoint.proceed();System.out.println("---环绕后---");System.out.println(proceed);} }
测试: 注意给UserServiceImpl添加注解
package com.shuilidianli.test; import com.shuilidianli.service.UserService; import com.shuilidianli.web.EmpController; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class AopTest2 {@Testpublic void test1(){ApplicationContext ctx =new ClassPathXmlApplicationContext("beans.xml");UserService us =(UserService) ctx.getBean("userServiceImpl");us.search(); } }
测试结果:
1.4.4 案例4
1)EmpController.java
package com.sldl.controller; import org.springframework.stereotype.Controller; @Controller //添加Bean注解 public class EmpController { public void findAll(){System.out.println("---正在查询所有员工信息---");} public void addEmp(){System.out.println("---正在添加一个员工信息---");String str = null;System.out.println(str.length());} }
2)Operation.java
package com.sldl.log; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Component // 添加Bean注解 @Aspect // 添加Aop注解 public class OperationLog {//后置通知注解@After("within(com.sldl.controller..*)")public void log(){System.out.println("记录日志");}//环绕通知注解@Around("within(com.sldl.controller..*)")public Object log1(ProceedingJoinPoint p) throws Throwable{//获取目标组件的名字String className = p.getTarget().getClass().getName();//获取目标组件里执行的方法名String methodName = p.getSignature().getName();System.out.println("------------");//执行目标组件Object obj = p.proceed();System.out.println("xxx正在执行"+className+"里的"+methodName+"方法");return obj;}//异常抛出通知@AfterThrowing(pointcut="within(com.sldl.controller..*)",throwing="e")public void log2(Exception e){System.out.println(e.toString());StackTraceElement[] eles =e.getStackTrace();System.out.println(eles[0]);System.out.println(eles[1]);} }
3)beans.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:context="http://www.springframework.org/schema/context"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!--开启注解扫描功能--><context:component-scan base-package="com.sldl"/> <!--开启AOP注解扫描--><aop:aspectj-autoproxy proxy-target-class="true"/> </beans>
4)AOPTest
package com.sldl.test; import com.sldl.controller.EmpController; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class AOPTest {@Testpublic void test1(){ApplicationContext ctx =new ClassPathXmlApplicationContext("beans.xml"); EmpController ec =ctx.getBean("empController", EmpController.class); //ec.findAll();ec.addEmp(); } }
测试结果:
1.4.5 多个通知的执行顺序
五个通知都有的情况下,先后执行顺序
-
一定限制性环绕前一步:
-
再执行前置通知
-
目标方法
-
环绕通知的后一部分
-
在执行后置通知
-
在执行返回通知
-
最后执行异常通知