之前的文章中我们介绍了Spring的控制反转和依赖注入,今天来详细说说Spring的aop。
aop(Aspect Oriented Programming)即面向切面编程,它是Spring提出的一种思想,是为了在不修改源代码的基础上对原代码进行增强,Spring aop是aop思想的实现。
我们知道java一贯接收的是OOP(Object Oriented Programming,面向对象开发)思想,在开发过程中一般遵循的是controller -> service -> dao层级开发,这种自上而下的开发思想可以让我们在开发过程中更加的清晰认识到层级之间的调用关系,但是这也存在一个明显的缺点,他只能从上而下不能定义横向的业务,比如我想在所有的service中都打印一句话,当service被调用的时候就打印这句话。在传统的OOP思想中,我们需要在每个service都先打印这句话,这样未免太过于臃肿,因此就需要引入aop的思想来弥补OOP中的不足了。
在aop中引入切面的思想,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
在使用切面思想时,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
一、aop中的核心概念
切面关注点:对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为切面关注的点。
切面(aspect):类是对物体特征的抽象,切面就是对切面关注点的抽象,可以理解是对切入点+增强(通知)的描述。
连接点(joinpoint):被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器。
切入点(pointcut):对连接点进行拦截的定义。
通知(advice):所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类。
目标对象:代理的目标对象。
织入(weave):将切面应用到目标对象并导致代理对象创建的过程。
引入(introduction):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段。
增强:一个具体的功能,实际上可以理解是通知的具体内容。
二、Spring中的aop
aop和Spring中aop的关系:
1、aop编程思想不是Spring独有的,Spring只是支持了aop变成思想,切勿搞反了两者之间的关系;
2、在aop的编程思想的加持下,aop可以支持对切面中类、方法、文件、参数、返回值等等类型进行拦截、改变、执行、增强的操作,而Spring中的aop支持的是方法级别的类型操作;
3、aop属于一种思想,而Spring的aop则是这个思想的实现。
Spring的aop底层默认由Java的动态代理来创建aop代理,如果需要代理的是类而不是接口的时候,Spring会自动的切换成CGLIB代理,不过也可以强制使用CGLIB代理,至于两者如何切换将在后续文章中讨论。除此之外,Spring的aop也需要IOC容器负责生成、管理,其依赖关系同样由IOC容器负责管理,因此,Spring中的aop可以直接使用IOC中的其他bean作为目标,这种关系仍然由IOC容器依赖注入提供。
三、Spring中aop的使用
3.1、xml版
3.1.1、基础配置
1.引入相关依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.sxx</groupId>
<artifactId>custom</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<spring.version>5.1.6.RELEASE</spring.version>
<junit.version>4.12</junit.version>
<slf4j.version>1.7.35</slf4j.version>
<aspectjweaver.version>1.8.9</aspectjweaver.version>
<json.version>1.2.27</json.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectjweaver.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${json.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>
</project>
2.创建日志对象
public class SysLog {
//请求ip
private String ip;
//操作用户
private String userName;
//请求方法名
private String methodName;
//请求入参
private String params;
//执行耗时
private Long dateTime;
//创建时间
private Date createdTime;
//操作类型
private String operation;
}
3.配置注解(为了在通知时获取更加全面注解信息,可以忽略)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ACL {
//请求方法名
String value();
//请求所需权限
String scope() default "";
//请求所需角色
String role() default "";
//是否打印日志
boolean printLog() default true;
}
4.配置controller对象及请求方法
public class UserController {
@ACL(value = "getUserInfo", scope = "普通权限", role = "普通用户")
public String getUserInfo(String id){
return "用户:" + id + "登录成功!";
}
}
3.1.2、配置通知类型
public class AspectHandle {
/**
* 前置通知
*/
public void beforeMethod() {
System.out.println("前置通知执行了");
}
/**
* 后置通知
*/
public void afterMethod() {
System.out.println("后置通知执行了");
}
/**
* 最终通知
*/
public void returningMethod() {
System.out.println("最终通知执行了");
}
/**
* 异常通知
*/
public void throwingMethod() {
System.out.println("异常通知执行了");
}
}
3.1.3、xml中配置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: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
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="aspectHandle" class="com.sxx.service.config.AspectHandle"/>
<bean id="userController" class="com.sxx.service.controller.UserController"/>
<!--aop配置-->
<aop:config>
<!--
配置切点表达式:
expression:切点表达式
* :匹配一个或者多个
.. :代表0个或者多个
-->
<aop:pointcut id="pt" expression="execution(* com.sxx.service.controller.*.* (..))"/>
<!--aop:aspect:配置切面信息,可以配置多个-->
<aop:aspect id="handle" ref="aspectHandle">
<!--前置通知-->
<aop:before method="beforeMethod" pointcut-ref="pt"/>
<!--后置通知-->
<aop:after method="afterMethod" pointcut-ref="pt"/>
<!--最终通知-->
<aop:after-returning method="returningMethod" pointcut-ref="pt"/>
<!--异常通知-->
<aop:after-throwing method="throwingMethod" pointcut-ref="pt"/>
</aop:aspect>
</aop:config>
</beans>
3.1.4、配置测试类
package com.sxx.web;
import com.sxx.service.controller.UserController;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class UserControllerTest {
private static final Logger LOGGER = LoggerFactory.getLogger(UserControllerTest.class);
@Test
public void test01(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:/bean.xml");
UserController userController = (UserController) applicationContext.getBean("userController");
String userInfo = userController.getUserInfo("123");
LOGGER.info(userInfo);
}
}
3.1.5、打印结果
前置通知执行了
后置通知执行了
最终通知执行了
异常通知同理,只需要在执行过程中抛出异常即可测试
3.1.6、环绕通知
环绕通知是四大通知的集合体,可以在环绕通知中加入四大通知,不同的是环绕通知必须要执行ProceedingJoinPoint中的proceed方法才能执行目标方法,其他和四大通知一样,在使用中要注意这一点。
通知类型改为:
package com.sxx.service.config;
import com.alibaba.fastjson.JSON;
import com.sxx.service.annotation.ACL;
import com.sxx.service.entity.SysLog;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Date;
public class AspectHandle {
/**
* 环绕通知,即包含了四大通知
*
* @param pjp
*/
public void aroundMethod(ProceedingJoinPoint pjp) {
System.out.println("环绕通知执行了。。。");
try {
System.out.println("前置通知执行了。。。");
printLog(pjp);
Object proceed = pjp.proceed();
System.out.println("执行结果为:" + JSON.toJSONString(proceed));
System.out.println("后置通知执行了。。。");
} catch (Throwable throwable) {
System.out.println("异常通知。。。");
System.out.println(throwable.getMessage());
}finally {
System.out.println("最终通知执行了。。。");
}
}
/**
* 打印相关信息
* @param pjp
* @throws NoSuchMethodException
*/
public void printLog(ProceedingJoinPoint pjp) throws NoSuchMethodException {
long start = System.currentTimeMillis();
//获取类的字节码对象,通过字节码对象获取方法信息
Class<?> targetCls = pjp.getTarget().getClass();
//获取方法签名(通过此签名获取目标方法信息)
MethodSignature ms = (MethodSignature) pjp.getSignature();
//获取目标方法上的注解指定的操作名称
Method targetMethod = targetCls.getDeclaredMethod(
ms.getName(),
ms.getParameterTypes());
ACL acl = targetMethod.getAnnotation(ACL.class);
String operation = acl.value();
System.out.println("targetMethod=" + targetMethod);
//获取目标方法名(目标类型+方法名)
String targetClsName = targetCls.getName();
String targetObjectMethodName = targetClsName + "." + ms.getName();
//获取请求参数
String targetMethodParams = Arrays.toString(pjp.getArgs());
//2.封装用户行为日志(SysLog)
SysLog entity = new SysLog();
entity.setIp("127.0.0.1");
entity.setUserName("admin");
entity.setMethodName(targetObjectMethodName);
entity.setParams(targetMethodParams);
entity.setDateTime(System.currentTimeMillis() - start);
entity.setCreatedTime(new Date());
entity.setOperation(operation);
//3.将日志写入到数据库
System.out.println(JSON.toJSONString(entity));
}
}
xml配置文件为
<bean id="aspectHandle" class="com.sxx.service.config.AspectHandle"/>
<bean id="userController" class="com.sxx.service.controller.UserController"/>
<!--aop配置-->
<aop:config>
<!--
配置切点表达式:
expression:切点表达式
* :匹配一个或者多个
.. :代表0个或者多个
-->
<aop:pointcut id="pt" expression="execution(* com.sxx.service.controller.*.* (..))"/>
<!--aop:aspect:配置切面信息,可以配置多个-->
<aop:aspect id="handle" ref="aspectHandle">
<!--环绕通知-->
<aop:around method="aroundMethod" pointcut-ref="pt"/>
</aop:aspect>
</aop:config>
执行结果如下:
环绕通知执行了。。。
前置通知执行了。。。
targetMethod=public java.lang.String com.sxx.service.controller.UserController.getUserInfo(java.lang.String)
{"createdTime":1645005033614,"dateTime":2,"ip":"127.0.0.1","methodName":"com.sxx.service.controller.UserController.getUserInfo","operation":"getUserInfo","params":"[123]","userName":"admin"}
执行结果为:"用户:123登录成功!"
后置通知执行了。。。
最终通知执行了。。。
3.2、注解版
注解版与xml思路一样,主要改动在xml配置文件和AspectHandle文件中
3.2.1、四大通知注解版
AspectHandle:
package com.sxx.service.config;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class AspectHandle {
//定义公共的切点表达式
@Pointcut("execution(* com.sxx.service.controller.*.* (..))")
public void pt() {
}
/**
* 前置通知
*/
@Before("pt()")
public void beforeMethod() {
System.out.println("前置通知执行了");
}
/**
* 后置通知
*/
@AfterReturning("pt()")
public void afterMethod() {
System.out.println("后置通知执行了");
}
/**
* 最终通知
*/
@After("pt()")
public void returningMethod() {
System.out.println("最终通知执行了");
}
/**
* 异常通知
*/
@AfterThrowing("pt()")
public void throwingMethod() {
System.out.println("异常通知执行了");
}
}
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"
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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--添加spring扫描,范围是com.sxx下所有包及其子包-->
<context:component-scan base-package="com.sxx"/>
<!--.启用@AsjectJ支持-->
<aop:aspectj-autoproxy/>
</beans>
执行结果如下:
前置通知执行了
最终通知执行了
后置通知执行了
注意:当使用注解版本的四大通知时,Spring是有bug的(最终执行会显示在最后执行之前)。
3.2.2、环绕通知注解版
AspectHandle:
package com.sxx.service.config;
import com.alibaba.fastjson.JSON;
import com.sxx.service.annotation.ACL;
import com.sxx.service.entity.SysLog;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Date;
@Component//交给Spring容器管理
@Aspect//标注为切面
public class AspectHandle {
//定义公共的切点表达式
@Pointcut("execution(* com.sxx.service.controller.*.* (..))")
public void pt(){
}
/**
* 环绕通知,即包含了四大通知
*
* @param pjp
*/
@Around("pt()")
public void aroundMethod(ProceedingJoinPoint pjp) {
System.out.println("环绕通知执行了。。。");
try {
System.out.println("前置通知执行了。。。");
printLog(pjp);
Object proceed = pjp.proceed();
System.out.println("执行结果为:" + JSON.toJSONString(proceed));
System.out.println("后置通知执行了。。。");
} catch (Throwable throwable) {
System.out.println("异常通知。。。");
System.out.println(throwable.getMessage());
}finally {
System.out.println("最终通知执行了。。。");
}
}
/**
* 打印相关信息
* @param pjp
* @throws NoSuchMethodException
*/
public void printLog(ProceedingJoinPoint pjp) throws NoSuchMethodException {
long start = System.currentTimeMillis();
//获取类的字节码对象,通过字节码对象获取方法信息
Class<?> targetCls = pjp.getTarget().getClass();
//获取方法签名(通过此签名获取目标方法信息)
MethodSignature ms = (MethodSignature) pjp.getSignature();
//获取目标方法上的注解指定的操作名称
Method targetMethod = targetCls.getDeclaredMethod(
ms.getName(),
ms.getParameterTypes());
ACL acl = targetMethod.getAnnotation(ACL.class);
String operation = acl.value();
System.out.println("targetMethod=" + targetMethod);
//获取目标方法名(目标类型+方法名)
String targetClsName = targetCls.getName();
String targetObjectMethodName = targetClsName + "." + ms.getName();
//获取请求参数
String targetMethodParams = Arrays.toString(pjp.getArgs());
//2.封装用户行为日志(SysLog)
SysLog entity = new SysLog();
entity.setIp("127.0.0.1");
entity.setUserName("admin");
entity.setMethodName(targetObjectMethodName);
entity.setParams(targetMethodParams);
entity.setDateTime(System.currentTimeMillis() - start);
entity.setCreatedTime(new Date());
entity.setOperation(operation);
//3.将日志写入到数据库
System.out.println(JSON.toJSONString(entity));
}
}
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"
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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--添加spring扫描,范围是com.sxx下所有包及其子包-->
<context:component-scan base-package="com.sxx"/>
<!--.启用@AsjectJ支持-->
<aop:aspectj-autoproxy/>
</beans>
执行结果如下:
环绕通知执行了。。。
前置通知执行了。。。
targetMethod=public java.lang.String com.sxx.service.controller.UserController.getUserInfo(java.lang.String)
{"createdTime":1645009250206,"dateTime":1,"ip":"127.0.0.1","methodName":"com.sxx.service.controller.UserController.getUserInfo","operation":"getUserInfo","params":"[123]","userName":"admin"}
执行结果为:"用户:123登录成功!"
后置通知执行了。。。
最终通知执行了。。。