目录
六、基于 AspectJ 实现 AOP 操作(配置文件方式)
一、AOP 简介
1、什么是 AOP
(1)AOP 就是面向切面编程
利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
(2)简单来说,就是不需要修改源代码,但依然可以为原来的代码添加新功能
比如在登录功能的基础上,添加一个权限检查的模块。通过某些配置,将这个模块(或部分实现代码)添加到登录功能上。
二、AOP 底层原理
1、动态代理原理
利用Java的反射技术(Java Reflection),在运行时创建一个实现某些给定接口的新类(也称“动态代理类”)及其实例(对象)。代理的是接口(Interfaces),不是类(Class),也不是抽象类。
AOP 底层是通过动态代理实现的,而动态代理是基于反射来设计的。动态代理有两种情况:
- 有接口,则使用基于接口的 JDK 动态代理
- 没有接口,则使用基于继承的 CGLib 动态代理
2、基于接口的 JDK 动态代理
创建接口实现类的代理(Proxy)对象,使用这个对象的 invoke 方法来增强接口实现类的方法(无论调用哪个方法都会增强)。
3、基于继承的 CGLib 动态代理
创建子类的代理对象,增强类的方法。
三、底层原理实现—— JDK 动态代理
1、使用 Proxy 类的方法创建代理对象
使用 newProxyInstance() 返回指定接口的代理类的实例,将该接口实例的方法调用分配给指定的调用处理程序。
经此步骤,在原本的方法的基础上,就会添加上增强的部分。
(1)newProxyInstance 方法的三个参数:
- ClassLoader,类加载器;
- interfaces,需要增强的方法所在的接口类,支持多个接口(数组形式);
- InvocationHandler,调用处理器(程序);
(2)对第一个参数的理解
上文提到动态代理的原理,而这个类加载器其实就是基于这个原理,将增强部分与原部分得到的结果赋予这个新类,那么我们调用这个新类的方法就可以得到我们想要的增强效果。
(3)对第二个参数(特别是多个接口的情况)的理解
newProxyInstance 是为一个实现类的实例来添加增强部分的,因为明确了具体哪一个实现类,也就明确了具体的方法。
又因为一个实现类很可能是多个接口的实现类,那么在这种情况下,就需要把所有接口都传入。
(4)对第三个参数的理解
调用处理器,它其实是一个接口。
我们实现这个接口,比如叫做 A,将实现类的实例传递给 A,在 invoke 方法中进行具体操作。
2、JDK 动态代理示例
目的:增强 UserDao 里的方法。先编写好基本的接口和实现类,然后给实现类增加新的方法。
(1)代码
(1-1)创建接口,定义方法
package com.demo.dao;
public interface UserDao {
public int add(int a,int b);
public String update(String id);
}
(1-2)创建接口实现类,实现方法
package com.demo.dao.impl;
import com.demo.dao.UserDao;
public class UserDaoImpl implements UserDao {
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public String update(String id) {
return id;
}
}
(1-3)使用 Proxy 类创建接口代理对象
package com.demo.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
// 创建代理对象的代码
public class JDKProxy implements InvocationHandler {
// 创建的是谁的代理对象,就把谁传递过来,一般用有参构造
private Object obj;
public JDKProxy(Object obj) {
this.obj = obj;
}
@Override // invoke 放在在代理对象创建后马上调用
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 增强之前
System.out.println(method.getName() + " 增强之前");
Object res = method.invoke(obj, args);
// 增强之后
System.out.println(method.getName() + " 增强之后");
return res;
}
}
(1-4)测试代码
import com.demo.dao.UserDao;
import com.demo.dao.impl.UserDaoImpl;
import com.demo.proxy.JDKProxy;
import org.junit.Test;
import java.lang.reflect.Proxy;
public class ProxyTest {
@Test
public void test() {
Class[] interfaces = {
UserDao.class
};
UserDao userDao = new UserDaoImpl();
UserDao userDaoProxy = (UserDao) Proxy.newProxyInstance(userDao.getClass().getClassLoader(), interfaces, new JDKProxy(userDao));
System.out.println("res = " + userDaoProxy.add(2, 3));
System.out.println("res = " + userDaoProxy.update("114514"));
}
}
(2)输出结果
四、AOP 操作术语
1、连接点
可以被增强的类方法,就称为连接点。
2、切入点
实际被增强的类方法,称为切入点(通过切入点表达式确定,后面会讲)。
3、通知(增强)
实际被增强的逻辑部分(代码),就称为通知。
通知有 5 种类型:
- 前置通知,原方法之前执行;
- 后置通知,原方法之后执行;
- 环绕通知,原方法之前和之后都执行;
- 异常通知,原方法异常时执行;
- 最终通知,类似 finally;
4、切面
切面是一个动作,是一个把通知(增强)应用到切入点的过程。(比如:把权限判断加入到登录这一过程,就是切面)
五、基于 AspectJ 实现 AOP 操作(注解)
前面所讲的 JDK 动态代理,是为了说明 AOP 是如何实现的。在实际应用中,不会使用这种方式实现 AOP 操作,而是通过 AspectJ 注解莱实现,对象的获取还是通过 IOC 来实现。
1、准备工作
(1)Spring 框架一般都是基于 AspectJ 实现 AOP 操作
- AspectJ 不是 Spring 组成部分,而是一个独立的 AOP 框架,一般把 AspectJ 和 Spirng 框架一起使用,进行 AOP 操作。
(2)基于 AspectJ 实现 AOP 操作的两种方式
- 基于 xml 配置文件实现;
- 基于注解方式实现(常用);
(3)引入相关依赖(仅写出了 AOP 部分所需依赖)
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.22</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.22</version>
</dependency>
<dependency>
<groupId>net.sourceforge.cglib</groupId>
<artifactId>com.springsource.net.sf.cglib</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.6.8</version>
</dependency>
</dependencies>
(4)切入点表达式
(4-1)切入点表达式的作用
- 知道对哪个类里面的哪个方法进行增强。
(4-2)语法结构
execution([权限修饰符][返回类型][类全路径][方法名称]([参数列表]))
- 权限修饰符:private、public、……;
- 返回类型:void、int、省略、……;
- 参数列表:..(两个点表示方法中的参数);
- *:表示任意权限修饰符、类、方法;
(4-3)例子
注意 * 后的空格是不能省略的,它代表了返回类型。
- 对 com.demo.dao.BookDao 类里面的 add() 进行增强:
execution(* com.demo.dao.BookDao.add(..))
- 对 com.demo.dao.BookDao 类里面的所有的方法进行增强:
execution(* com.demo.dao.BookDao.*(..))
- 对 com.demo.dao 包里面所有类,类里面所有方法进行增强:
execution(* com.atguigu.dao.*.*(..))
2、基于 AspectJ 注解方式
(1)开启注解扫描以及生成代理对象
- 可以使用配置类,也可以使用配置文件。两个标签的作用类似,就是寻找给定范围内的类是否包含对应标签。
<?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">
<!-- 注解扫描 -->
<context:component-scan base-package="com.demo"></context:component-scan>
<!-- 开启Aspect生成代理对象 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
(2)创建类,定义方法
package com.demo.dao.impl;
import com.demo.dao.UserDao;
import org.springframework.stereotype.Component;
@Component
public class UserDaoImpl implements UserDao {
@Override
public void add() {
System.out.println("add()......");
}
}
(3)创建 Proxy 类(编写增强逻辑),并添加注解 @Aspect
- 创建不同通知类型的方法,添加对应的注解(注解是 aspect 包中的注解),并使用切入点表达式确定目标方法。
package com.demo.proxy;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class UserDaoProxy {
@Before(value = "execution(* com.demo.dao.impl.UserDaoImpl.add(..))")
public void before() { // 前置通知
System.out.println("before()......");
}
}
(4)测试代码
import com.demo.dao.UserDao;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class AspectBeanTest {
@Test
public void test() {
ApplicationContext context = new ClassPathXmlApplicationContext("AspectBean.xml");
UserDao userDao = context.getBean("userDaoImpl", UserDao.class);
userDao.add();
}
}
(5)输出结果
3、其他通知
(1)代码
package com.demo.proxy;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class UserDaoProxy {
@Before(value = "execution(* com.demo.dao.impl.UserDaoImpl.add(..))")
public void before() { // 前置通知
System.out.println("前置通知......");
}
@After(value = "execution(* com.demo.dao.impl.UserDaoImpl.add(..))")
public void after() { // finally 通知
System.out.println("finally 通知......");
}
@AfterReturning(value = "execution(* com.demo.dao.impl.UserDaoImpl.add(..))")
public void afterReturning() { // 后置通知
System.out.println("后置通知......");
}
@AfterThrowing(value = "execution(* com.demo.dao.impl.UserDaoImpl.add(..))")
public void afterThrowing() { // 异常通知
System.out.println("异常通知......");
}
@Around(value = "execution(* com.demo.dao.impl.UserDaoImpl.add(..))")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { // 环绕通知
System.out.println("环绕通知之前......");
proceedingJoinPoint.proceed();
System.out.println("环绕通知之后......");
}
}
(2)输出结果
(3)出现异常的输出结果
4、公共切入点提取
上面的示例代码中的切入点的 value 值都一样,可以将他们提取出来。
@Pointcut(value = "execution(* com.demo.dao.impl.UserDaoImpl.add(..))")
public void AddPoint() {
}
@Before(value = "AddPoint()")
public void before() { // 前置通知
System.out.println("前置通知......");
}
除此之外,还可以为每个方法加上自定义注解,然后在代理类中用如下方法标记:
5、多个 Proxy 类增强同一个方法
如果出现多个 Proxy 增强类都含有多同一个方法的增强,那么可以通过设置优先级来确定它们的执行(增强)顺序。
(1)在 Proxy 增强类上添加注解 @Order
- @Order(整数值),其中整数值越小,该增强类的优先级越大。
(2)代码
package com.demo.proxy;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Aspect
@Order(1)
public class UserDaoProxy {
@Before(value = "execution(* com.demo.dao.impl.UserDaoImpl.add(..))")
public void before() { // 前置通知
System.out.println("前置通知......");
}
}
@Component
@Aspect
@Order(0)
class Person {
@Before(value = "execution(* com.demo.dao.impl.UserDaoImpl.add(..))")
public void before() {
System.out.println("person 的前置通知");
}
}
(3)输出结果
6、完全注解开发
(1)创建配置类,不需要创建 xml 配置文件
package com.demo.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan(basePackages = {"com.demo"})
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class Config {
}
(2)测试代码
import com.demo.config.Config;
import com.demo.dao.UserDao;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class AspectBeanTest {
@Test
public void test() {
ApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
UserDao userDao = context.getBean("userDaoImpl", UserDao.class);
userDao.add();
}
}
(3)输出结果
六、基于 AspectJ 实现 AOP 操作(配置文件方式)
Proxy类和目标增强类的对象的创建就是 IOC 里讲的操作,重点在于 AOP 部分的配置。
1、示例
(1)代码
(1-1)Book 类和 BookProxy 类
package com.demo.pojo;
public class Book {
public void buy() {
System.out.println("but()......");
}
}
package com.demo.proxy;
public class BookProxy {
public void before() {
System.out.println("before 前置通知");
}
}
(1-2)配置文件
<?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 对象 -->
<bean id="book" class="com.demo.pojo.Book"></bean>
<bean id="bookProxy" class="com.demo.proxy.BookProxy"></bean>
<!-- 配置 AOP 增强 -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut id="pc" expression="execution(* com.demo.pojo.Book.buy(..))"/>
<!-- 配置切面 -->
<aop:aspect ref="bookProxy">
<!-- 配置增强作用的具体方法 -->
<aop:before method="before" pointcut-ref="pc"></aop:before> <!-- 表示把 before() 作用到 pc 指向的方法上 -->
</aop:aspect>
</aop:config>
</beans>
(1-3)测试代码
import com.demo.pojo.Book;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class XmlTest {
@Test
public void test() {
ApplicationContext context = new ClassPathXmlApplicationContext("Bean01.xml");
Book book = context.getBean("book", Book.class);
book.buy();
}
}
(2)输出结果