Spring AOP
AOP(Aspect Oriented Programming)的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。-- 摘自百度百科
1、JDK动态代理
JDK
动态代理回顾(实现动态代理的经典口诀):
- 创建接口,定义目标方法要完成的功能
- 创建接口实现类
- 创建
InvocationHandler
接口实现类 在invoke()
方法中完成代理类的功能(调用目标方法 + 功能增强) - 使用
Proxy类
的静态方法创建代理对象并把返回值转为接口类型
例如:
/**
* @description: JDK动态代理回顾
* @author: laizhenghua
* @date: 2022/5/22 19:34
*/
public class ProxyTest {
public static void main(String[] args) {
// 创建目标对象
Counter factory = new CounterImpl();
LengthHandler handler = new LengthHandler(factory);
// 4.使用Proxy类的静态方法创建代理对象并把返回值转为接口类型
Counter counter = (Counter) Proxy.newProxyInstance(factory.getClass().getClassLoader(), factory.getClass().getInterfaces(), handler);
// 调用目标方法 测试功能增强
String str = "hello";
int length = counter.getLength(str);
System.out.printf("[%s] length is [%d]", str, length);
}
}
// 1.创建接口 定义目标方法要完成的功能(例如:目标方法的功能是计算字符串的长度)
interface Counter {
int getLength(String str);
}
// 2.创建接口实现类
class CounterImpl implements Counter {
@Override
public int getLength(String str) {
if (str == null || str.length() == 0) {
return 0;
}
return str.length();
}
}
// 3.创建InvocationHandler接口实现类 在invoke方法中完成代理类的功能(调用目标方法 + 功能增强)
class LengthHandler implements InvocationHandler {
private Object target = null;
public LengthHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 3.1 调用目标方法
Object result = method.invoke(target, args);
// 3.2 功能增强
if (Integer.parseInt(result.toString()) != 0) {
result = Integer.parseInt(result.toString()) + 2;
System.out.println("哈哈 想不到吧!在不改变源功能代码的情况下 长度多了2");
}
return result;
}
}
了解JDK
的动态代理后,我们就知道在Spring中AOP编程是如何增强方法功能的。当然除了JDK
动态代理外,还有一种在框架中常见的代理方式,就是CGLIB
。两种代理方式最主要的区别是:
JDK
动态代理:面向接口即必须要有接口才能实现CGLIB
:面向类通过继承实现,即不用实现接口也能代理,专门做动态代理,更加优秀
回顾AOP:
为什么要使用AOP?对对象执行方法进行横切面的拦截处理,对目标方法的业务进行织入逻辑增强处理。言外之意接口实现类的目标方法只需要关系具体需要实现的业务,其他附属业务如验证参数、限流处理、登录校验、安全性处理、用户访问日志记录、发送消息、性能统计、事务处理等非接口实现类目标方法该关注的逻辑或处理,都应该由开发者自定义的切面去实现,来降低模块之间的耦合度,使系统更容易扩展更好的复用代码。
AOP相关概念:
横切关注点:跨越应用程序多个模块的方法或功能。即是与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志 ,安全 , 缓存,事务等等 …
切面(Aspect):横切关注点被模块化 的特殊对象。即,它是一个类。
通知(Advice):(就是增强)。切面必须要完成的工作。即,它是类中的一个方法。
目标(Target):被通知对象。
代理(Proxy):向目标对象应用通知之后创建的对象。
切入点(PointCut):(就是实际被真正增强的方法)。切面通知 执行的 “地点”的定义。
连接点(JointPoint):与切入点匹配的执行点。
AOP
总体来说,各种概念还是比较多,这里就不一一介绍了。这里主要介绍AOP
使用方式帮助大家在实际开发中快速搭建AOP
环境。
2、AOP的使用
2.1、AOP前置知识
1、jar
依赖坐标引入(Spring在内部定义了AOP规范:例如事务处理Spring使用的是自己封装的AOP,我们很少使用Spring自己封装的AOP,因为比较笨重。业内使用AspectJ
开源AOP框架比较多,因此项目上如果需要支持AOP需要单独引入aspectj
依赖,spring-boot-starter-aop
集成了aspectj
)
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.5.6</version>
</dependency>
补充:在Spring
中使用AOP
开发时,一般使用AspectJ
的实现方式。AspectJ
是Eclipse
基金会专门做AOP
的开源项目,优秀至极扩展了Java语言提供了强大的切面实现。
2、Advice
通知常用的注解
注解 | 说明 |
---|---|
@Before | 前置通知 |
@AfterReturning | |
@Around | 环绕通知 |
@AfterThrowing | |
@After | 后置通知 |
3、切入点表达式定义(告诉执行的方法,那些方法是符合这个表达式的条件,如果符合则进入通知如果不符合就直接返回处理方法)
AspectJ
定义了专门表达式用于指定切入点,表达式的原型是:
execution(
modifiers-pattern? ret-type-parttern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
)
/*
modifiers-pattern: 方法访问权限类型
ret-type-parttern: 返回值类型
declaring-type-pattern: 包名类名
name-pattern(param-pattern): 方法名(参数类型和参数个数)
throws-pattern: 抛出异常类型
?: 表示可写的部分
execution(访问权限 方法返回值 方法声明(参数) 异常类型)
*/
时刻记住execution
表达式想表述的就是方法的签名。各部分间用空格隔开,在其中可以使用特殊符号(parttern
):
符号 | 含义 |
---|---|
* | 0至多个任意字符 |
.. | 用在方法参数中,表示任意多个参数。用在包名,表示当前包及其子包路径 |
+ | 用在类名,表示当前类及其子类。用在接口,表示当前接口及其实现类 |
举例
// 任意公共方法
execution(public * *(..))
// 任何一个以 set 开头的方法
execution(* set*(..))
// UserServiceImpl类的所有delete()方法
execution(* com.laizhenghua.cache.service.impl.UserServiceImpl.delete(..))
// UserServiceImpl类的delete(List<Integer> idList)方法,注意:如果方法中的入参类型是java.lang包下的类,可以直接使用类名,否则必须使用全限定类名
execution(* com.laizhenghua.cache.service.impl.UserServiceImpl.delete(java.util.List))
// service包或者子包里的任意类的任意方法
execution(* *..service.*.*(..))
// 只有一级包下的service子包下所有类(接口)中所有方法的切入点
execution(* *.service.*.*(..))
AOP切入点的定义,还可以使用注解定义!这种定义方式更加简单与灵活,例如注解不仅可以作为切入点的描述我还能在注解中嵌入一些附加信息!去切面类中灵活处理。
@Pointcut("@annotation(com.laizhenghua.cache.annotation.CacheBatchEvict)")
public void pointcut() {
// 只要在方法中添加 @CacheBatchEvict 注解就能做AOP处理
}
4、方法参数匹配
1、在AOP中代理对象执行目标方法,也可以把目标方法的参数传给代理对象,这是毋庸置疑的。即可以在切入点表达式中增加args()
部分,表示目标方法除了要满足execution
部分外,还要满足args()
对方法参数的要求,对于符合execution
表达式但是不符合args
参数的方法,不会被织入切面。
2、声明了args()
之后,就可以把目标方法的参数传入到切面方法的参数中(通过环绕通知ProceedingJoinPoint
也可以获取参数,是一个Object类型的数组,不好处理,不像args()
直接用切面方法参数接收与匹配)例如:
/**
* @description: 登录切面类
* @author: laizhenghua
* @date: 2022/5/17 20:33
*/
@Aspect // 代表当前类是一个切面类
@Component(value = "loginAspect")
public class LoginAspect {
private final org.apache.logging.log4j.Logger log = Logger.getLogger(LoginAspect.class);
// value编辑一个表达式 告诉执行的方法 那些方法是符合这个表达式的条件 如果符合则进入通知 如果不符合就直接返回处理方法
@Pointcut(value = "execution(* com.laizhenghua.cache.service.impl.UserServiceImpl.login(..)) && args(username, password)")
public void loginPointCut(String username, String password) {
}
/**
* 前置通知
*/
@Before(value = "loginPointCut(username, password)")
public void loginBeforeAdvice(String username, String password) {
log.info("hello " + username);
}
}
3、注意目标方法参数传递会经历两个过程
- 目标方法参数通过参数顺序传入
args()
args()
参数通参数名称传入增强方法中
4、但是常用AOP注解中还有一个属性就是argNames
,如果设置了这个属性Spring不再使用方法参数名来匹配,而是使用argNames
定义的顺序来赋值,例如
@After(value = "loginPointCut(username, password)", argNames = "password, username")
public void loginAfterAdvice(String username, String password) {
log.info("save login log -> " + username); // 日志会输出密码而不是用户名
}
// 注意看 argNames 的定义顺序,此时会把目标方法的 password 参数赋值给增强方法的 username 参数
了解这些必要知识后,我们再来看项目中如何使用AOP进行开发呢?为了介绍方便给出两种实现方式:
1、SpringBoot
中通过纯注解的方式使用AOP
2、原生Spring
中通过纯XML
配置文件实现
2.2、在SpringBoot中使用AOP
在SpringBoot
中使用AOP更简单,只需要掌握几个注解就能完成AOP开发。
1、引入jar
包依赖
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.5.6</version>
</dependency>
2、切面类定义。注意注解@Aspect、@Pointcut、@Before、@Around、@After
的使用。特别是@Pointcut()
加上此注解后连接点就会很灵活,不仅能定义多个,而且还能在其他地方复用与组合其他连接点,例如:
@Pointcut(value = "execution(* com.laizhenghua.cache.service.impl.UserServiceImpl.save(..))")
public void savePointCut() {
}
@Pointcut(value = "execution(* com.laizhenghua.cache.service.impl.UserServiceImpl.delete(..))")
public void deletePointCut() {
}
@Before(value = "savePointCut() || deletePointCut()") // 连接点 逻辑或(or) 组合
public void before() {
}
@After(value = "savePointCut() && deletePointCut()") // 连接点 逻辑且(and) 组合
public void after() {
}
切面类定义示例:
/**
* @description: 登录切面类
* @author: laizhenghua
* @date: 2022/5/17 20:33
*/
@Aspect // 代表当前类是一个切面类
@Component(value = "loginAspect")
public class LoginAspect {
private final org.apache.logging.log4j.Logger log = Logger.getLogger(LoginAspect.class);
// value编辑一个表达式 告诉执行的方法 那些方法是符合这个表达式的条件 如果符合则进入通知 如果不符合就直接返回处理方法
@Pointcut(value = "execution(* com.laizhenghua.cache.service.impl.UserServiceImpl.login(..)) && args(username, password)")
public void loginPointCut(String username, String password) {
}
/**
* 前置通知
*/
@Before(value = "loginPointCut(username, password)")
public void loginBeforeAdvice(String username, String password) {
log.info("hello " + username);
// 前置通知可校验参数是否合理等
}
/**
* 后置通知
*/
@After(value = "loginPointCut(username, password)", argNames = "username, password")
public void loginAfterAdvice(String username, String password) {
log.info("save login log -> " + username);
// 后置通知主要做登录日志记录
}
}
3、在SpringBoot使用AOP就是这么简单,连接点的定义还是推荐使用自定义注解的方式。只需在方法中加上我们自己的注解就能做AOP处理,还可以在自己的注解中嵌入额外的信息在切面类中灵活处理。
例如:批量删除数据接口,批量删除数据的同时,我们也想批量删除缓存中的数据就可以使用自定义的注解+AOP实现(Spring缓存抽象中的@CacheEvict
只支持删除单个key)详细实现文章如下:
文章地址:https://blog.csdn.net/m0_46357847/article/details/124954143
2.3、在原生Spring中使用AOP
1、项目搭建(略)
2、引入AOP支持依赖坐标(注意有版本兼容问题,需要和Spring一致)
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.12.RELEASE</version>
</dependency>
3、编写业务功能的接口和接口实现类
/**
* @description: UserService
* @author: laizhenghua
* @date: 2022/5/28 14:41
*/
public interface UserService {
UserEntity getById(Integer id);
}
接口实现类,主要用于测试,我们尽量写简单一点
/**
* @description:
* @author: laizhenghua
* @date: 2022/5/28 14:41
*/
@Service(value = "userService")
public class UserServiceImpl implements UserService {
@Override
public UserEntity getById(Integer id) {
System.out.printf("SQL -> SELECT * FROM USER U WHERE U.ID = %d", id);
UserEntity entity = new UserEntity();
entity.setId(id);
entity.setUsername("admin");
entity.setPassword("123");
return entity;
}
}
4、切面类编写,也就是增强类,具体的增强功能都写在切面类里。注意为了规范可以新建一个handler
包,把所有切面类都放在这个包下
/**
* @description: 日志输出增强类
* @author: laizhenghua
* @date: 2022/5/28 14:47
*/
public class LogAspect {
/**
* 环绕通知
* @param joinPoint
*/
public Object proceed(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); // 方法签名
Method method = methodSignature.getMethod(); // 方法
Object[] args = joinPoint.getArgs(); // 方法参数
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date = format.format(new Date());
System.out.println(String.format("invoke log: %s %s is run, args is %s", date, method.getName(), Arrays.toString(args)));
// 这里我们能拿到所有的方法信息,可以灵活处理,完成各种各样的功能例如参数校验、缓存清除等一切目标方法之前之前或执行之后想做的事情
return joinPoint.proceed();
}
/**
* 前置通知
*/
public void before() {
}
/**
* 后置通知
*/
public void after() {
}
}
5、编写spring-aop.xml
配置文件,注册增强类的bean
以及AOP
配置
spring-aop.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="logAspect" class="com.laizhenghua.ehcache.handler.LogAspect"/>
<!-- ====== AOP配置开始 ====== -->
<aop:config>
<!-- 自定义的切面 -->
<aop:aspect ref="logAspect">
<!-- 切入点 -->
<aop:pointcut id="pointcut" expression="execution(* com.laizhenghua.ehcache.service.impl.UserServiceImpl.getById(Integer))"/>
<!-- 通知 -->
<aop:before method="before" pointcut-ref="pointcut"/>
<aop:after method="after" pointcut-ref="pointcut"/>
<aop:around method="proceed" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
<!--<aop:aspectj-autoproxy proxy-target-class="true"/>-->
<!-- ====== AOP配置结束 ====== -->
</beans>
6、最把spring-aop.xml
配置文件在applicationContext.xml
导入即可,而applicationContext.xml
是在web.xml
的<init-param>
中配置的。这样tomcat容器启动的时候会读取配置文件内容,将bean
创建好并载入spring
容器中。
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 开启注解支持 -->
<context:annotation-config/>
<!-- 指定扫描的package -->
<context:component-scan base-package="com.laizhenghua.ehcache"/>
<import resource="classpath:spring/spring-test.xml"/>
<import resource="classpath:spring/spring-aop.xml"/>
<import resource="classpath:spring/spring-mvc.xml"/>
<import resource="classpath:spring/spring-ehcache.xml"/>
</beans>
7、测试
/**
* @description: UserController
* @author: laizhenghua
* @date: 2022/5/28 14:38
*/
@RestController
@RequestMapping(value = "user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping(value = "/get/{id}", method = RequestMethod.GET)
public R getById(@PathVariable("id") Integer id) {
return R.ok().put("data", userService.getById(id));
}
}
访问API
观察日志输出
8、小结
除了以上自定义切面类实现功能增强外,我们还可以使用Spring
提供的API实现。例如:
package com.laizhenghua.ehcache.handler;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
/**
* @description:
* @author: laizhenghua
* @date: 2022/5/28 19:25
*/
public class BeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
// 功能增强
System.out.println(target.getClass().getName() + "类的" + method.getName() + "方法被执行了");
}
}
在把增强类BeforeAdvice
在配置文件中配置即可,例如
<?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="beforeAdvice" class="com.laizhenghua.ehcache.handler.BeforeAdvice"/>
<aop:config>
<aop:pointcut id="pointcut" expression="execution(* com.laizhenghua.ehcache.service.impl.UserServiceImpl.getById(Integer))"/>
<aop:advisor advice-ref="beforeAdvice" pointcut-ref="pointcut"/>
</aop:config>
</beans>
单元测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"classpath:applicationContext.xml"})
public class Test {
@Autowired
private UserService userService;
@org.junit.Test
public void test() {
UserEntity entity = userService.getById(1);
System.out.println(entity);
}
}
日志输出