什么是Aop?
让我们首先定义一些核心的AOP概念和术语。这些术语并非特定于Spring。不幸的是,AOP术语并不是特别直观。然而,如果Spring使用自己的术语,则会更加令人困惑。
- 方面(Aspect)︰跨越多个类的模块化关注点。事务管理是企业Java应用程序中横切关注点的一个很好的例子。在SpringAOP中,方面是通过使用常规类(基于模式的方法)或使用@Aspect注解(@Aspectj样式)注解的常规类来实现的。
- 连接点(Join point)︰程序执行过程中的一点,如方法的执行或异常的处理。在SpringAOP中,连接点总是表示一个方法执行。
- 通知(Advice) : 一个方面在特定连接点采取的行动。不同类型的通知包括"环绕"、"前“和"后"通知。许多AOP框架(包括Spring)将通知建模为拦截器,并在连接点周围维护拦截器链。
- 切点(Pointcut)︰与连接点匹配的谓词。通知与切入点表达式关联,并在与切入点匹配的任何连接点上运行(例如,使用特定名称执行方法)。pointcut表达式匹配的连接点概念是AOP的核心,Spring默认使用AspectJ pointcut表达式语言。
- 说明(Introduction)∶代表类型声明其他方法或字段。SpringAOP允许你向任何advised对象引入新的接口(和相应的实现)。例如,你可以使用一个Introduction使bean实现一个lsModified接口,以简化缓存。(introduction在AspectJ社区中称为类型间声明。)
- 目标对象(Target object):由一个或多个方面advised的对象。也称为"advised对象"。因为SpringAOP是通过使用运行时代理实现的,所以这个对象始终是一个代理对象。
- AOP代理∶由AOP框架创建的用于实现aspect contracts(通知方法执行等)的对象。在Spring框架中,AOP代理是JDK动态代理或CGLIB代理。
- 编织(Weaving):将aspects与其他应用程序类型或对象链接,以创建advised的对象。这可以在编译时(例如,使用AspectJ编译器)、加载时或运行时完成。Spring AOP和其他纯Java AOP框架一样,在运行时进行编织。
Spring AOP包含以下几种通知类型:
- Before advice:在连接点之前运行但不能阻止执行到连接点的通知(除非它抛出异常)。
- After returning advice:在连接点正常完成后要运行的通知(例如,如果方法返回并且不引发异常)
- After throwing advice:如果方法通过引发异常而退出,则要执行的通知。
- After (finally) advice:无论连接点退出的方式如何(正常或异常返回),都要执行的通知。
- Around advice:环绕连接点(如方法调用)的通知。这是最有力的通知。around通知可以在方法调用前后执行自定义行为。它还负责通过返回自己的返回值或引发异常来选择是继续到连接点还是快捷地执行通知的方法。Around advice是最普遍的advice。由于Spring AOP和AspectJ一样提供了一系列完整的通知类型,我们建议你使用功能最差的通知类型来实现所需的行为。例如,如果只需要使用方法的返回值更新缓存,那么最好实现after retuming advice,而不是around advice,尽管around advice可以完成相同的事情。使用最具体的通知类型提供了一个更简单的编程模型,并且错误的可能性更小。例如,你不需要在用于around通知的joinpoint上调用proceed()方法,因此,你不会调用失败。
Spring AOP是纯Java实现的。不需要特殊的编译过程。SpringAOP不需要控制类加载器层次结构,因此适合在Servlet容器或应用程序服务器中使用。
AOP代理
SpringAOP默认为对AOP代理使用标准的JDK动态代理。这样就可以代理任何接口(或一组接口)。
SpringAOP也可以使用CGLIB代理。如果要代理类而不是接口,则必须使用CGLIB。默认情况下,如果业务对象不实现接口,则使用cglib。由于编程到接口而不是类是很好的实践,业务类通常实现一个或多个业务接口。在某些情况下(可能很少),需要通知一个接口上没有声明的方法,或者需要将代理对象作为具体类型传递给方法,则可以强制使用cglib。
定义一个切入点(Pointcut)
切入点决定连接点,从而使我们能够控制何时执行通知。SpringAOP只支持SpringBean的方法执行连接点,因此你可以将切入点视为匹配SpringBean上方法的执行。一个切入点声明有两部分:一个包含一个名称和任何参数的签名,一个能精确地确定我们感兴趣的执行方法的切入点表达式。在aop的@Aspectj注解样式中,通过常规方法定义提供切入点签名,并使用@Pointcut注解指示切入点表达式(作为切入点签名的方法必须具有void返回类型)。
支持的切入点指示符
SpringAOP支持在切入点表达式中使用的以下AspectJ切入点指示符(PCD):
- execution:用于匹配方法执行连接点。这是使用SpringAOP时要使用的主要切入点指示符。within:限制匹配到特定类型中的连接点(使用SpringAOP时,在匹配类型中声明的方法的执行)
- this:将匹配限制为连接点(使用SpringAOP时方法的执行),其中bean引用(SpringAOP代理)是给定类型的实例。
- target:限制匹配到连接点(使用SpringAOP时方法的执行),其中目标对象(要代理的应用程序对象)是给定类型的实例。
- args:限制匹配到联接点(使用SpringAOP时方法的执行),其中参数是给定类型的实例。
- @target:限制匹配到连接点(使用SpringAOP时方法的执行),其中执行对象的类具有给定类型的注解。
- @args:限制匹配到联接点(使用SpringAOP时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注解。
- @within:限制与具有给定注解的类型中的联接点匹配(使用SpringAOP时,在具有给定注解的类型中声明的方法的执行)。
- @annotation:限制匹配到连接点的主题(在SpringAOP中执行的方法)具有给定注解的连接点。
一个例子可能有助于清晰地区分切入点签名和切入点表达式。下面的示例定义一个名为anyOldTransfer的切入点,该切入点与名为Transfer的任何方法的执行相匹配︰
@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature
组合切入点表达式
你可以使用&&、|和!组合切入点表达式。也可以按名称引用切入点表达式。下面的示例显示三个切点表达式:
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
共享公共切入点定义
在处理企业应用程序时,开发人员通常希望从多个方面引用应用程序的模块和特定的操作集。我们建议定义一个"SystemArchitecture"方面,该方面为此目的捕获公共切入点表达式。此类方面通常类似于以下示例:
package com.xyz.someapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SystemArchitecture {
/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.someapp.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.web..*)")
public void inWebLayer() {
}
/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.someapp.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.service..*)")
public void inServiceLayer() {
}
/**
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.someapp.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.dao..*)")
public void inDataAccessLayer() {
}
/**
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
* <p>
* If you group service interfaces by functional area (for example,
* in packages com.xyz.someapp.abc.service and com.xyz.someapp.def.service) then
* the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
* could be used instead.
* <p>
* Alternatively, you can write the expression using the 'bean'
* PCD, like so "bean(*Service)". (This assumes that you have
* named your Spring service beans in a consistent fashion.)
*/
@Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
public void businessService() {
}
/**
* A data access operation is the execution of any method defined on a
* dao interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
public void dataAccessOperation() {
}
}
除返回类型模式(前面代码段中的ret-type-pattern)、名称模式和参数模式之外的所有部分都是可选的。确定方法的返回类型,以便匹配连接点。*最常用作返回类型模式。它匹配任何返回类型。只有当方法返时,完全限定的类型名才匹配。名称模式与方法名匹配。你可以使用*通配符作为名称模式的全部或部分。如果指定声明类型模式,请包含后缀.将其连接到名称模式组件。参数模式稍微复杂一点:()匹配不带参数的方法,而(..)匹配任何数量(零个或多个)的参数。(*)模式与采用任何类型参数的方法匹配。(*,string)匹配接受两个参数的方法。第一个可以是任何类型,而第二个必须是字符串。有关更多信息,请参阅AspectJ编程指南的语言语义部分。
以下示例显示了一些常见的切入点表达式:
- 执行任何公共方法:
execution(public **(..))
- 执行任何以set开头的方法︰
execution( * set*(..))
- 执行任何定义在AccountService类的方法:
execution( * com.xyz.service.AccountService.*(..))
- 执行任何定义在service包中的方法:
execution(* com.xyz.service.*.*(..))
- 执行任何定义在service包或者他的一个子包中的方法:
execution( * com.xyz.service..*.*(..))
- 任何在service包中的连接点(仅仅是Spring AOP中执行的方法)
within(com.xyz.service.*)
- service包或其子包中的任何连接点(仅在SpringAOP中执行的方法)︰
within(com.xyz.service..*)
- 任何实现了AccountService接口的代理连接点(仅在SpringAOP中执行的方法)∶
this(com.xyz.service.AccountService)
- 任何目标对象有@Transactional注解的连接点(仅在SpringAOP中执行的方法)︰
@target(org.springframework.transaction.annotation.Transactional)
- 目标对象的声明类型具有@transactional注解的任何连接点(仅在Spring AOP中执行的方法)∶
@within(org.springframework.transaction.annotation.Transactional)
- 执行方法具有@transactional注解的任何连接点(仅在Spring AOP中执行方法)∶
@annotation(org.springframework.transaction.annotation.Transactional)
声明通知
通知与切入点表达式关联,并在切入点匹配的方法执行before, after,或 around运行。切入点表达式可以是对命名切入点的简单引用,也可以是就地声明的切入点表达式。如下所示:
Before Advice
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
//...
}
}
After Returning Advice
当一个匹配的方法正常返回时执行After returming advice。你可以通过@AfterReturning注解来声明,你可以在同一方面内拥有多个通知声明(以及其他成员)。在这些示例中,我们只显示一个通知声明,以集中显示每个通知声明的效果。
有时,你需要访问通知主体中返回的实际值。你可以使用@AfterReturn的形式绑定返回值以获得该访问,如下示例所示:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
//...
}
@AfterReturning(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
returning属性中使用的名称必须与advice方法中参数的名称相对应。当方法执行返回时,返回值作为相应的参数值传递给通知方法。返回子句还限制只匹配那些返回指定类型值的方法执行(在本例中是Object,它匹配任何返回值)。
After Throwing Advice
当匹配的方法抛出异常退出时,After throwing advice执行,你可以使用@AfterThrowing来声明它:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
//当匹配的方法抛出异常退出时,After throwihg advice执行
@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}
/**
* 抛出给定类型的异常时才运行通知,并且通常还需要访问通知正文中抛出的异常。
* 可以使用throwing属性来限制匹配(如果需要-使用throwable作为异常类型),
* 并将引发的异常绑定到advice参数。以下示例显示了如何执行此操作:
*/
@AfterThrowing(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
throwing属性中使用的名称必须与advice方法中参数的名称相对应。当通过引发异常退出方法执行时,异常作为相应的参数值传递给通知方法。throwing子句还限制只匹配那些引发指定类型的异常(在本例中是DataAccessException)的方法执行。
After (Finally) Advice
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
Around Advice
最后一种advice是around advice的。around advice运行"around"匹配方法的执行。它有机会在方法执行之前和之后都进行工作,并确定何时、如何以及即使该方法真正开始执行。如果需要以线程安全的方式(例如,启动和停止计时器)共享方法执行前后的状态,则通常使用around建议。始终使用满足你要求的最不强大的advice形式(也就是说,如果在around之前不使用around)。使用@Around注解声明around通知。advice方法的第一个参数必须是ProceedingJoinPoint类型。在通知正文中,对ProceedingJoinPoint调用proceed()会导致执行基础方法。proceed方法也可以传入Object]。数组中的值在方法执行过程中用作参数。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
around通知返回的值是方法调用方看到的返回值。例如,一个简单的缓存方面可以从缓存中返回一个值(如果有),如果没有,则调用proceed ()。请注意,可以在around建议的主体中调用proceed一次、多次或根本不调用proceed。所有这些都是合法的。
访问当前JoinPoint
任何advice方法都可以将org.aspectilang.joinpoint类型的参数声明为其第一个参数(请注意,需要使用around advice从而声明ProceedingJoinPoint类型作为第一个参数,该参数是JoinPoint的子类)。JoinPoint接口提供了许多有用的方
法:
- getArgs():返回方法参数
- getThis():返回代理对象
- getTarget():返回目标对象
- getSignature():返回被advice的方法描述.
- toString():打印被advice方法的有用描述
在SpringBoot中使用
pom
<!-- 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.3.6.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.11.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
通知
package com.dev.aop.aspect;
import com.dev.aop.annotation.Log;
import com.dev.aop.util.ServletUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* @Description : spring aop //描述
*/
@Aspect
@Component
@Slf4j
public class LogAspect {
/* 常量 */
private static final String HTTP_POST = "POST";
private static final String HTTP_GET = "GET";
/**
* 切入点
*/
@Pointcut(value = "@annotation(com.dev.aop.annotation.Log)")
public void logPointcut(){
}
/**
* 后置处理(方法返回时触发)
* @param joinPoint
* @param object
*/
@AfterReturning(value = "logPointcut()",returning = "object")
public void logAfterReturning(JoinPoint joinPoint, Object object) {
resolver(joinPoint, object, null);
}
/**
* 异常通知
* @param joinPoint
* @param e
*/
@AfterThrowing(value = "logPointcut()",throwing = "e")
public void logAfterThrowing(JoinPoint joinPoint,Exception e) {
resolver(joinPoint, null, e);
}
/**
* 解析参数,保存日志
* @param joinPoint
* @param object
* @param e
*/
protected void resolver(final JoinPoint joinPoint,final Object object, Exception e) {
try {
//获取注解
Log log = (Log) getAnnotationLog(joinPoint);
if (Objects.isNull(log)) {
return;
}
/**
* 获取登录用户session
* 如果整合shiro使用: User user = (User) SecurityUtils.getSubject().getPrincipal()
*/
User user = (User) ServletUtil.getSession().getAttribute("user");
//假设map为log类
Map<String,Object> map = new HashMap<String, Object>(16);
if (Objects.nonNull(user)) {
//操作人
map.put("oper_user", user.getUsername());
//部门
map.put("oper_dep", user.getDep());
}
//请求主机ip:127.0.0.1
//如果整合shiro使用:getSubject().getSession().getHost()
map.put("ip", getHostIp());
//状态:成功/失败
map.put("status", 1);
//请求URL
map.put("req_url", ServletUtil.getRequest().getRequestURI());
//请求方式:GET/POST
map.put("req_method", ServletUtil.getRequest().getMethod());
//类名
String className = joinPoint.getTarget().getClass().getName();
//方法名称
String methodName = joinPoint.getSignature().getName();
//设置方法名称
map.put("oper_method", className + "." + methodName + "()");
//返回结果
map.put("json_result", marshal(object));
//处理注解参数
getAnnotationParameter(log,map,joinPoint);
if (Objects.nonNull(e)) {
//状态:成功/失败
map.put("status", 2);
//异常信息
map.put("err_msg", substring(e.toString(), 0, 3000));
}
//------------------------异步插入数据库-----------------------------
System.out.println(map.toString());
} catch (Exception exception) {
exception.printStackTrace();
System.out.println(exception.getMessage());
}
}
/**
* 注解参数处理
*/
public void getAnnotationParameter(Log log, Map<String, Object> m, JoinPoint joinPoint) throws Exception {
//标题
m.put("title", log.title());
//操作类别:INSERT、UPDATE
m.put("business", log.type().ordinal());
//是否保存请求参数
if (log.isParameter()){
getParameter(m,joinPoint);
}
}
/**
* 获取注解
*/
private Log getAnnotationLog(JoinPoint joinPoint) throws Exception {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
return method.getAnnotation(Log.class);
}
return null;
}
/**
* 获取并解析请求参数
*/
private void getParameter(Map<String, Object> map, JoinPoint joinPoint) throws Exception {
String method = ServletUtil.getRequest().getMethod();
//POST
if (HTTP_POST.equals(method)) {
Object[] args = joinPoint.getArgs();
if (args != null || args.length > 0){
map.put("req_parameter", substring(marshal(args), 0, 3000));
}
}
//GET
if (HTTP_GET.equals(method)) {
Map<String, String[]> parameterMap = ServletUtil.getRequest().getParameterMap();
if (!CollectionUtils.isEmpty(parameterMap)) {
map.put("req_parameter", substring(marshal(parameterMap), 0, 3000));
}
}
}
private String marshal(Object value) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
ObjectWriter objectWriter = objectMapper.writerWithDefaultPrettyPrinter();
return objectWriter.writeValueAsString(value);
}
/**
* 截取字符串
*/
public static String substring(final String str, int start, int end) {
if (str == null) {
return "";
}
if (end < 0) {
end = str.length() + 1 + end;
}
if (start < 0) {
start = str.length() + start;
}
if (end > str.length()) {
end = str.length();
}
if (start > end) {
return "";
}
if (start < 0) {
start = 0;
}
if (end < 0) {
end = 0;
}
return str.substring(start, end);
}
//可以忽略不计
@Data
public class User implements Serializable {
private int id;
private String username;
private String dep;
public User(int id, String username, String dep) {
this.id = id;
this.username = username;
this.dep = dep;
}
}
/**
* 获取ip
*/
public static String getHostIp() {
try {
return InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
}
return "127.0.0.1";
}
}
测试
package com.dev.aop.controller;
import com.dev.aop.annotation.Log;
import com.dev.aop.aspect.LogAspect;
import com.dev.aop.util.ServletUtil;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* @Description : 测试 //描述
*/
@RestController
public class IndexController {
@Log(title = "测试Post", type = Log.BusinessType.INSERT, isParameter = true)
@PostMapping("/Post")
public String indexPost(@RequestBody Map<String, Object> map) {
System.out.println(map.toString());
ServletUtil.getSession().setAttribute("user",new LogAspect().new User(1,"admin","技术部"));
return "success";
}
@Log(title = "测试Get", type = Log.BusinessType.OTHER, isParameter = true)
@GetMapping("/Get")
public String indexGet(Map<String, Object> map) {
System.out.println(map.toString());
ServletUtil.getSession().setAttribute("user",new LogAspect().new User(1,"admin","技术部"));
return "success";
}
@Log(title = "测试err", type = Log.BusinessType.OTHER, isParameter = true)
@PostMapping("/err")
public String indexErr(@RequestBody Map<String, Object> map) {
System.out.println(map.toString());
ServletUtil.getSession().setAttribute("user",new LogAspect().new User(1,"admin","技术部"));
throw new RuntimeException("NullPointException");
}
}
测试还是老方式
generated-requests.http
###
POST http://localhost/Post
Content-Type: application/json
{
"admin": 5,
"name": "张三"
}
1打印的是请求参数,2处理过后封装好参数
###
POST http://localhost/err
Content-Type: application/json
{
"admin": 2,
"name": "kangkang"
}
可以从2处看到参数"err_msg"中已经获取到控制台打印的异常信息
参考资料:Spring5中文参考指南
GitHub:传送门