AOP
实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。
在运行期间通过代理方式向目标类植入增强的代码 有两种实现方式:基于接口的 JDK 动态代理和基于继承的 CGLIB 动态代理。
AOP使用场景
a. 作⽤:AOP技术,利利⽤用⼀一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了了多个类的公共⾏行行为封装到⼀个可重⽤用模块,并将其名为“Aspect”,即⽅方⾯面。所谓“⽅方⾯面”,简单地说,就是将那些与业务⽆无关,却为业务模块所共同调⽤用的逻辑或责任封装起来,便便于减少系统的重复代码,降低模块间的耦合度,并有利利于未来的可操作性和可维护性。
b. 实现:
i. ⼀一是采⽤用动态代理理技术,利利⽤用截取消息的⽅方式,对该消息进⾏行行装饰,以取代原有对象⾏行行为的执⾏行行;
ii. ⼆二是采⽤用静态织⼊入的⽅方式,引⼊入特定的语法创建“⽅方⾯面”,从⽽而使得编译器器可以在编译期间织⼊入有关“⽅方⾯面”的代
码。
c.AOP用来封装横切关注点,具体可以在下面的场景中使用:
-
Authentication 权限
-
Caching 缓存
-
Context passing 内容传递
-
Error handling 错误处理
-
Lazy loading 懒加载
-
Debugging 调试
-
logging, tracing, profiling and monitoring 记录跟踪 优化 校准
-
Performance optimization 性能优化
-
Persistence 持久化
-
Resource pooling 资源池
-
Synchronization 同步
-
Transactions 事务
最常见的一些横切行为如下面这些:
- 日志记录,跟踪,优化和监控
- 事务的处理
- 持久化
- 性能的优化
- 资源池,如数据库连接池的管理
- 系统统一的认证、权限管理等
- 应用系统的异常捕捉及处理
d.AOP相关概念
这里只是重温 AspectJ 中几个必须要了解的概念:
Aspect: Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。
Joint point:表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。
Pointcut:表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
Advice:Advice 定义了在 pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。
-
切面(Aspect):一个关注点的模块化,这个关注点实现可能另外横切多个对象。事务管理是J2EE应用中一个很好的横切关注点例子。方面用Spring的 Advisor或拦截器实现。
-
连接点(Joinpoint): 程序执行过程中明确的点,如方法的调用或特定的异常被抛出。
-
通知(Advice): 在特定的连接点,AOP框架执行的动作。各种类型的通知包括“around”、“before”和“throws”通知。通知类型将在下面讨论。许多AOP框架包括Spring都是以拦截器做通知模型,维护一个“围绕”连接点的拦截器链。Spring中定义了四个advice: BeforeAdvice, AfterAdvice, ThrowAdvice和DynamicIntroductionAdvice
-
切入点(Pointcut): 指定一个通知将被引发的一系列连接点的集合。AOP框架必须允许开发者指定切入点:例如,使用正则表达式。 Spring定义了Pointcut接口,用来组合MethodMatcher和ClassFilter,可以通过名字很清楚的理解, MethodMatcher是用来检查目标类的方法是否可以被应用此通知,而ClassFilter是用来检查Pointcut是否应该应用到目标类上
-
引入(Introduction): 添加方法或字段到被通知的类。 Spring允许引入新的接口到任何被通知的对象。例如,你可以使用一个引入使任何对象实现 IsModified接口,来简化缓存。Spring中要使用Introduction, 可有通过DelegatingIntroductionInterceptor来实现通知,通过DefaultIntroductionAdvisor来配置Advice和代理类要实现的接口
-
目标对象(Target Object): 包含连接点的对象。也被称作被通知或被代理对象。POJO
-
AOP代理(AOP Proxy): AOP框架创建的对象,包含通知。 在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
-
织入(Weaving): 组装方面来创建一个被通知对象。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。
何时使用JDK还是CGLiB?
1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP。
2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP。
3、如果目标对象没有实现了接口,必须采用CGLIB库,Spring会自动在JDK动态代理和CGLIB之间转换。
1.JDK动态代理
Spring JDK 动态代理需要实现 InvocationHandler 接口,重写 invoke 方法,客户端使用 Java.lang.reflect.Proxy 类产生动态代理类的对象。
代码示例:
public class Book {
private int id;
private String name;
private String author;
public Book(int id, String name, String author) {
this.id = id;
this.name = name;
this.author = author;
}
@Override
public String toString() {
return "Book{" +
"id=" + id +
", name='" + name + '\'' +
", author='" + author + '\'' +
'}';
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
定义两个接口:
package com.yxkj.spring_aop_demo.service;
import com.yxkj.spring_aop_demo.entity.Book;
import java.util.List;
public interface BookService {
Book selectById(int id);
List<Book> selectAll();
void addBook(Book book);
void updateBook(Book book);
}
public interface BookChildService {
Book selectCilidByid(int id);
}
实现类:
public class BookServiceImpl implements BookService,BookChildService {
@Override
public Book selectById(int id) {
System.out.println("selectById正在执行根据ID查询Book数据@");
return null;
}
@Override
public List<Book> selectAll() {
System.out.println("selectAll正在执行查询所有数据!");
return null;
}
@Override
public void addBook(Book book) {
System.out.println("addBook执行添加Book操作!");
System.out.println("name="+book.getName()+";author="+book.getAuthor());
}
@Override
public void updateBook(Book book) {
System.out.println("updateBook执行修改操作!");
System.out.println("name="+book.getName()+";author="+book.getAuthor());
}
@Override
public Book selectCilidByid(int id) {
System.out.println("selectCilidByid查询数据");
return null;
}
}
Aspect切面类,定义增强方法:
/**
* @USER:
* @DATE: 2021-07-14
* @description: MyAspect 类在切面中定义了两个增强的方法,分别为 myBefore 和 myAfter。
*/
public class MyAspect {
public void myBefore(){
System.out.println("---方法执行之前-----");
}
public void myAfter(){
System.out.println("----方法执行之后-----");
}
}
JDKProxy:
/**
* @USER:
* @DATE: 2021-07-14
* @description:Spring JDK 动态代理需要实现 InvocationHandler 接口,重写 invoke 方法,
* 客户端使用 Java.lang.reflect.Proxy 类产生动态代理类的对象。
*/
public class JdkProxy implements InvocationHandler {
private Object target; //需要代理的目标对象
final MyAspect myAspect = new MyAspect(); //切面实例
// 1.定义获取代理对象方法 客户端使用 Java.lang.reflect.Proxy 类产生动态代理类的对象。
public Object JDKProxy(Object targetObject){
this.target = targetObject; // 为目标对象target赋值
return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),targetObject.getClass().getInterfaces(),this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
myAspect.myBefore();//前置通知
Object obj = method.invoke(target,args);
myAspect.myAfter();
return obj;
}
}
测试:
public class TestJdkProxy {
@Test
public void test(){
JdkProxy jdkProxy = new JdkProxy();
BookService service = (BookService) jdkProxy.JDKProxy(new BookServiceImpl());
BookChildService bookChildService = (BookChildService) jdkProxy.JDKProxy(new BookServiceImpl());
Book book = new Book(1,"老人与海","哈哈哈");
service.selectById(1);
service.selectAll();
service.addBook(book);
service.updateBook(book);
System.out.println("-----------------------------------");
bookChildService.selectCilidByid(1);
}
}
测试结果
---方法执行之前-----
selectById正在执行根据ID查询Book数据@
----方法执行之后-----
---方法执行之前-----
selectAll正在执行查询所有数据!
----方法执行之后-----
---方法执行之前-----
addBook执行添加Book操作!
name=老人与海;author=哈哈哈
----方法执行之后-----
---方法执行之前-----
updateBook执行修改操作!
name=老人与海;author=哈哈哈
----方法执行之后-----
-----------------------------------
---方法执行之前-----
selectCilidByid查询数据
----方法执行之后-----
2. CGLIB动态代理
JDK 动态代理的目标类必须要实现一个或多个接口,具有一定的局限性,如果不希望实现接口,可以使用 CGLIB代理。
2.1 导入依赖
使用 CGLIB 需要导入 CGLIB 和 ASM 包,即 asm-x.x.jar 和 CGLIB-x.x.x.jar 。如果您已经导入了 Spring 的核心包 spring-core-x.x.x.RELEASE.jar,就不用再导入 asm-x.x.jar 和 cglib-x.x.x.jar 了。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.1.8.RELEASE</version>
<scope>compile</scope>
</dependency>
2.2创建接口
public interface BookService {
Book selectById(int id);
List<Book> selectAll();
void addBook(Book book);
void updateBook(Book book);
}
2.3 编写实现类
public class BookServiceImpl implements BookService {
@Override
public Book selectById(int id) {
System.out.println("selectById正在执行根据ID查询Book数据@");
return null;
}
@Override
public List<Book> selectAll() {
System.out.println("selectAll正在执行查询所有数据!");
return null;
}
@Override
public void addBook(Book book) {
System.out.println("addBook执行添加Book操作!");
System.out.println("name="+book.getName()+";author="+book.getAuthor());
}
@Override
public void updateBook(Book book) {
System.out.println("updateBook执行修改操作!");
System.out.println("name="+book.getName()+";author="+book.getAuthor());
}
}
2.4 编写Aspect切面类
public class MyAspect {
public void myBefore(){
System.out.println("---方法执行之前-----");
}
public void myAfter(){
System.out.println("----方法执行之后-----");
}
}
2.5 编写CGLIB类
public class MyCglib implements MethodInterceptor {
private Object target;//目标对象
final MyAspect myAspect = new MyAspect();//切面类
/**
* 定义代理对象方法
* @param objectTarget
* @return
*/
public Object getCglibProxy(Object objectTarget){
//为目标对象赋值
this.target = objectTarget;
Enhancer enhancer = new Enhancer();
//设置父类,因为CGLIB是针对指定的类生成一个子类,所以需要指定父类
enhancer.setSuperclass(objectTarget.getClass());
//设置回调
enhancer.setCallback(this);
Object object = enhancer.create();//创建并返回代理对象
return object;
}
/**
* 重写拦截方法
* @param o
* @param method
* @param objects
* @param methodProxy
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
myAspect.myBefore();
Object obj =method.invoke(target,objects);
myAspect.myAfter();
return obj;
}
}
2.6 测试
public class TestCglib {
@Test
public void test(){
MyCglib cglib = new MyCglib();//实例化CglibProxy对象
BookService service = (BookService) cglib.getCglibProxy(new BookServiceImpl());
Book book = new Book(1,"老人与海","哈哈哈");
service.selectById(1);
service.selectAll();
service.addBook(book);
service.updateBook(book);
System.out.println("-----------------------------------");
}
}
3. JDK代理和CGLIB代理的区别
JDK 动态代理只能对实现了接口的类生成代理,而不能针对类。
CGLIB 是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法。因为是继承,所以该类或方法不能声明成 final 类型。
JDK动态代理特点
- 代理对象必须实现一个或多个接口
- 以接口的形式接收代理实例,而不是代理类
CGLIB动态代理特点 - 代理对象不能被 final 修饰
- 以类或接口形式接收代理实例
JDK与CGLIB动态代理的性能比较 - 生成代理实例性能:JDK > CGLIB
- 代理实例运行性能:JDK > CGLIB
4. Spring集成AspectJ
4.1引入依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.4</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
4.2 使用 AspectJ 开发 AOP 通常有以下 2 种方式:
- 基于 XML 的声明式 AspectJ
- 基于 Annotation 的声明式 AspectJ
4.2.1基于AspectJ XML开发
基于 XML 的声明式是指通过 Spring 配置文件的方式来定义切面、切入点及通知,而所有的切面和通知都必须定义在 aop:config 元素中。
编写通知类
public class Logging {
/**
* 前置通知
*/
public void beforeAdvice(){
System.out.println("前置通知");
}
/**
* 后置通知
*/
public void afterAdive(){
System.out.println("后置通知");
}
/**
* 返回后通知
* @param retVal
*/
public void afterReturnAdvice(Object retVal){
System.out.println("返回值="+retVal);
}
/**
* 抛出异常通知
*/
public void afterThrowingAdvice(IllegalArgumentException e){
System.out.println("这里的异常为:"+e.toString());
}
}
编写实体类:
public class Person {
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
private String name;
private int age;
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public void throwException(){
System.out.println("抛出异常");
throw new IllegalArgumentException();
}
}
编写配置类: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:context="http://www.springframework.org/schema/context"
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/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="person" class="com.yxkj.spring_aop_demo.entity.Person">
<property name="name" value="hhhhhh"></property>
<property name="age" value="22"></property>
</bean>
<bean id="logging" class="com.yxkj.spring_aop_demo.entity.Logging"></bean>
<!--aop配置 -->
<aop:config>
<!--定义aop切面 -->
<aop:aspect id="log" ref="logging">
<!--定义切入点 -->
<aop:pointcut id="myPointCut" expression="execution(* com.yxkj.spring_aop_demo.*.*.*(..))"/>
<!--前置通知 -->
<aop:before method="beforeAdvice" pointcut-ref="myPointCut" ></aop:before>
<!-- 后置通知-->
<aop:after-returning method="afterReturnAdvice" returning="retVal" pointcut-ref="myPointCut"></aop:after-returning>
<!-- 异常通知-->
<aop:after-throwing method="afterThrowingAdvice" pointcut-ref="myPointCut" throwing="e"></aop:after-throwing>
<!--最终通知 -->
<aop:after method="afterAdive" pointcut-ref="myPointCut"></aop:after>
</aop:aspect>
</aop:config>
</beans>
测试:
public class TestAopXml {
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
Person person = context.getBean("person",Person.class);
String name = person.getName();
int age = person.getAge();
// System.out.println(name+":"+age);
//person.throwException();
}
}
4.2.2 定义切面aop:aspect
4.2.3 定义切入点aop:pointcut
aop:pointcut 用来定义一个切入点,当 aop:pointcut元素作为 aop:config 元素的子元素定义时,表示该切入点是全局切入点,它可被多个切面所共享;
当 aop:pointcut 元素作为 aop:aspect 元素的子元素时,表示该切入点只对当前切面有效。
<aop:config>
<aop:pointcut id="myPointCut"
expression="execution(* net.biancheng.service.*.*(..))"/>
</aop:config>
其中,id 用于指定切入点的唯一标识名称,execution 用于指定切入点关联的切入点表达式。
execution 格式为:
execution(modifiers-pattern returning-type-pattern declaring-type-pattern name-pattern(param-pattern)throws-pattern)
其中:
- returning-type-pattern、name-pattern、param-pattern 是必须的,其它参数为可选项。
- modifiers-pattern:指定修饰符,如 private、public。
- returning-type-pattern:指定返回值类型,*表示可以为任何返回值。如果返回值为对象,则需指定全路径的类名。
- declaring-type-pattern:指定方法的包名。
- name-pattern:指定方法名,代表所有,set 代表以 set 开头的所有方法。
- param-pattern:指定方法参数(声明的类型),(..)代表所有参数,()代表一个参数,(,String)代表第一个参数可以为任何值,第二个为 String 类型的值。
- throws-pattern:指定抛出的异常类型。
定义通知:
<aop:aspect id="myAspect" ref="aBean">
<!-- 前置通知 -->
<aop:before pointcut-ref="myPointCut" method="..."/>
<!-- 后置通知 -->
<aop:after-returning pointcut-ref="myPointCut" method="..."/>
<!-- 环绕通知 -->
<aop:around pointcut-ref="myPointCut" method="..."/>
<!-- 异常通知 -->
<aop:after-throwing pointcut-ref="myPointCut" method="..."/>
<!-- 最终通知 -->
<aop:after pointcut-ref="myPointCut" method="..."/>
....
</aop:aspect>
4.3 AspectJ基于注解开发AOP
AspectJ 框架为 AOP 开发提供了一套注解。
注解如下:
启用 @AspectJ 注解有以下两种方法:
1)使用@Configuration和@EnableAspectJAutoProxy注解
@Configuration
@EnableAspectJAutoProxy
public class Appconfig {
}
2)基于XML配置
<aop:aspectj-autoproxy>
定义切面@Aspect
@Aspect
public class AspectModule {
}
定义切入点@Pointcut
// 要求:方法必须是private,返回值类型为void,名称自定义,没有参数
@Pointcut("execution(*net.biancheng..*.*(..))")
private void myPointCut() {
}
定义通知advice
@Before("myPointCut()")
public void beforeAdvice(){
System.out.println("前置通知");
}
@AfterReturning(pointcut = "myPointCut()",returning = "retVal")
public void afterRetrunAdive(Object retVal){
System.out.println("后置通知="+retVal.toString());
}
@After("myPointCut()")
public void afterAdive(){
System.out.println("最终通知");
}
代码全部:
1.定义自动引入aop代理注解
@SpringBootApplication
@EnableAspectJAutoProxy
public class SpringAopDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringAopDemoApplication.class, args);
}
}
2.定义切面类:
@Aspect
@Component
public class AopAspectConfig {
@Pointcut("execution(* com.yxkj.spring_aop_demo.*.*.*(..))")
public void myPointcut(){
}
@Before("myPointcut()")
public void beforeAdive(){
System.out.println("beforeAdive前置通知");
}
@AfterReturning(pointcut = "myPointcut()",returning="object")
public void afterReturnAdvice(Object object){
System.out.println("后置返回通知="+object);
}
@After("myPointcut()")
public void afterAdvice(){
System.out.println("最终通知");
}
@AfterThrowing(value = "myPointcut()",throwing = "e")
public void throwingAdivce(Exception e){
System.out.println("异常通知!");
e.getMessage();
}
}
3.测试:
写一个controller
@RestController
@RequestMapping(value = "/login")
public class LongUserController {
@RequestMapping(value = "/index",method = RequestMethod.POST)
public String login(){
System.out.println("哈哈哈哈哈");
return "登录成功!";
}
@RequestMapping(value = "/error",method = RequestMethod.GET)
public String error(){
System.out.println("登录失败!");
return "登录失败!";
}
}
访问登录成功后的控制台:
beforeAdive前置通知
哈哈哈哈哈
最终通知
后置返回通知=登录成功!