我们开发的Web系统都会有日志模块,用来记录对数据有进行变更的操作。一般都会记录请求的URL,请求的IP,执行的方法,操作人员等等。其目的可能是为了保留操作痕迹,防抵赖,或是记录系统运行情况,再有就是审计要求。
360百科:在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
将一些功能从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。比如最常见的日志记录、性能统计、安全控制、事务处理、异常处理等等。
这里主要为 Spring Framework 5.2.5 中AOP的介绍。
说明:
环绕通知是最普遍的通知。因为Spring AOP和AspectJ一样,提供了各种各样的通知类型,所建议使用最不强大的通知类型来实现所需的行为。使用最特定的通知类型可以提供更简单的编程模型,减少出错的可能性。
所有的通知参数都是静态类型的,这样就可以使用适当类型的通知参数(例如:方法执行返回值的类型),而不是对象数组。
由切入点匹配的连接点的概念是AOP的关键,它将AOP与只提供拦截的旧技术区分开来。切入点使通知能够独立于面向对象的层次结构。例如,可以将提供声明性事务管理的环绕通知应用于一组跨多个对象的方法(例如:服务层中的所有业务操作)。
Spring AOP是在纯Java中实现的。不需要特殊的编译过程。Spring AOP不需要控制类装入器层次结构,因此适合在servlet容器或应用程序服务器中使用。
Spring AOP目前只支持方法执行连接点(建议在Spring bean上执行方法)。虽然可以在不破坏核心Spring AOP api的情况下添加对字段拦截的支持,但是没有实现字段拦截。如果需要通知字段访问和更新连接点,请考虑AspectJ之类的语言。
Spring框架的AOP功能通常与Spring IoC容器一起使用。切面是通过使用普通的bean定义语法来配置的(尽管这允许强大的“自动代理”功能)。这是与其他AOP实现的一个重要区别。
Spirng的AOP的动态代理实现机制有两种,分别是:JDK动态代理和CGLib动态代理。简单介绍下两种代理机制:
两者对比:
@AspectJ指的是将切面声明为用注解注释的常规Java类的样式。@AspectJ样式是由AspectJ项目作为AspectJ 5发行版的一部分引入的。Spring使用AspectJ提供的用于切入点解析和匹配的库来解释与AspectJ 5相同的注释。但是AOP运行时仍然是纯Spring AOP,并且不依赖于AspectJ编译器或编织器。
1.第一个示例展示应用程序上下文中的一个常规bean定义,它指向一个具有@Aspect注释的bean类。
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of the aspect here -->
</bean>
2.第二个示例展示NotVeryUsefulAspect类定义,它是由org.aspectj.lang.annotation.Aspect 注解。
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}
切面(使用@Aspect注解的类)可以具有与任何其他类相同的方法和字段。它们还可以包含切入点、通知和引入(类型间)声明。
通过组件扫描自动检测切面:可以将切面类注册为Spring XML配置中的常规bean,或者通过类路径扫描自动检测它们——与任何其他Spring管理的bean相同。但是,请注意@Aspect注解对于类路径中的自动检测是不够的。为此,您需要添加一个单独的@Component注解(或者,根据Spring的组件扫描器的规则,一个定制的原型注释)。
切入点确定感兴趣的连接点,从而使我们能够控制何时执行通知。Spring AOP只支持Spring bean的方法执行连接点,因此可以将切入点看作是与Spring bean上的方法执行相匹配的。切入点声明有两部分:一个签名,它包含名称和任何参数;一个是切入点表达式,它确定我们对哪个方法执行感兴趣。在AOP的@AspectJ注解风格中,切入点签名由一个常规方法定义提供,切入点表达式通过使用@Pointcut注解来表示(作为切入点签名的方法必须有一个void返回类型)。
@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature
切入点指示器
Spring AOP支持以下用于切入点表达式的AspectJ切入点指示器(PCD):
切入点表达式
可以使用&&、||和!组合切入点表达式。您还可以通过名称引用切入点表达式。
// 如果方法执行连接点表示任何公共方法的执行,则匹配。
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
// 如果方法执行在交易模块中,则匹配。
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}
// 如果方法执行代表交易模块中的任何公共方法,则匹配。
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
最佳实践是从较小的命名组件构建更复杂的切入点表达式,如前面所示。当通过名称引用切入点时,应用普通的Java可见性规则(您可以在相同的类型中看到私有切入点,层次结构中受保护的切入点,任何地方的公共切入点,等等)。可见性不影响切入点匹配。
通知与切入点表达式相关联,并在切入点匹配的方法执行之前、之后或前后运行。切入点表达式可以是对指定切入点的简单引用,也可以是在适当位置声明的切入点表达式。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
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的形式绑定返回值来获得访问权限。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}
}
通常,您希望仅在抛出给定类型的异常时才运行通知,并且常常需要在通知正文中访问抛出的异常。您可以使用throwing属性来限制匹配(如果需要,可以使用Throwable作为异常类型),并将抛出的异常绑定到一个通知参数。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
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() {
// ...
}
}
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;
}
}
开发工具:IDEA 2019.3.1
框架版本:SpringBoot 2.2.6
1.pom.xml中加入Spring AOP依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.启用@AspectJ支持,这里默认已经开启。
3.自定义日志注解类。
package com.liyafei.core.log;
import java.lang.annotation.*;
/**
* Title: WebLog
* Description: 自定义日志注解类
* Company: Ongoing蜗牛
*
* @author liyf
* @date 2020年3月28日
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.PARAMETER, ElementType.METHOD })
public @interface WebLog {
/**
* 渠道
* @return 渠道标识
*/
String channel() default "web";
/**
* 功能名称
* @return 功能名称
*/
String name() default "";
/**
* 方法名称
* @return 方法名称
*/
String action() default "";
/**
* 是否保存(默认不保存)
* @return 是否保存
*/
boolean saveFlag() default false;
}
@Retention 注解保留策略
RetentionPolicy 策略枚举类(RUNTIME 注解将由编译器记录在类文件中,并在运行时由VM保留,因此可以反射性地读取它们。)
@Target 注解目标位置(也就是该注解要用在什么地方)
ElementType 目标元素类型枚举类(PARAMETER:参数,METHOD:方法)
4.日志切面类
package com.liyafei.core.log;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import com.liyafei.skillset.domain.SysUserLog;
import com.liyafei.skillset.service.SysUserLogService;
import com.liyafei.skillset.tools.utils.RequestUtil;
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.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.alibaba.fastjson.JSONObject;
/**
* Title: WebLogAspect
* Description: 日志切面类
* Company: Ongoing蜗牛
*
* @author liyf
* @date 2020年3月28日
*/
@Component
@Aspect
public class WebLogAspect {
private static final Logger logger = LoggerFactory.getLogger(WebLogAspect.class);
private final SysUserLogService sysUserLogService;
public WebLogAspect(SysUserLogService sysUserLogService) {
this.sysUserLogService = sysUserLogService;
}
/**
* 连接点(切入点)
* 切入点表达式:匹配 web包及子包 Controller类的任何公共方法
*/
@Pointcut("execution(public * com.liyafei.skillset.web..*Controller.*(..))")
public void webLog() {
}
/**
* 通知:前置通知(Before advice),在连接点之前运行但不能阻止执行流继续到连接点的通知(除非它抛出异常)。
* 在日志文件或控制台输出请求信息
*
* @param joinPoint
*/
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) {
// 利用RequestContextHolder获取HttpServletRequest对象
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();
// 重组请求信息
StringBuffer sb = new StringBuffer();
sb.append("收到请求:");
sb.append("\r\n访问URI :" + httpServletRequest.getRequestURI().toString());
sb.append("\r\nSession :" + httpServletRequest.getSession().getId());
sb.append("\r\n访问IP :" + RequestUtil.getIP(httpServletRequest));
sb.append("\r\n响应类 :" + joinPoint.getSignature().getDeclaringTypeName());
sb.append("\r\n方法 :" + joinPoint.getSignature().getName());
Object[] objects = joinPoint.getArgs();
for (Object arg : objects) {
if (arg != null) {
sb.append("\r\n参数 :" + arg.toString());
}
}
// 打印请求信息
logger.info(sb.toString());
}
/**
* 通知:后置通知(After returning advice),通知在连接点正常完成后运行
* 处理请求日志信息
*
* @param joinPoint
*/
@AfterReturning(pointcut = "webLog()", returning = "rvt")
public void doAfterReturning(JoinPoint joinPoint, Object rvt) {
// 处理日志信息
handleLog(joinPoint, null);
}
/**
* 通知:异常通知(After throwing advice),方法通过抛出异常退出,则要执行的通知
* 处理请求异常日志信息
*
* @param joinPoint
* @param e
*/
@AfterThrowing(pointcut = "webLog()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
// 处理日志信息
handleLog(joinPoint, e);
}
/**
* 日志处理
*
* @param joinPoint
* @param e
*/
private void handleLog(JoinPoint joinPoint, Exception e) {
// 利用RequestContextHolder获取HttpServletRequest对象
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();
// 获取执行的方法
Signature signature = joinPoint.getSignature();
if(!(signature instanceof MethodSignature)) {
throw new IllegalArgumentException("暂不支持非方法注解");
}
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
// 获取注解
WebLog controllerLog = method.getAnnotation(WebLog.class);
if (controllerLog != null) {
// 保存日志到数据库
if (controllerLog.saveFlag()) {
SysUserLog sysUserLog = new SysUserLog();
// SessionId
sysUserLog.setAccount(httpServletRequest.getRequestedSessionId());
// 渠道
sysUserLog.setChannel(controllerLog.channel());
// 功能名称
sysUserLog.setName(controllerLog.name());
// 响应类.方法
sysUserLog.setAction(signature.getDeclaringTypeName() + "." + method.getName());
// URI
sysUserLog.setUrl(httpServletRequest.getRequestURI());
// 参数
sysUserLog.setParams(JSONObject.toJSONString(httpServletRequest.getParameterMap()).replace("\"", ""));
// 请求IP
sysUserLog.setIp(RequestUtil.getIP(httpServletRequest));
// 操作时间
sysUserLog.setLogTime(new Date());
// 异常信息
if (e != null) {
sysUserLog.setErrMsg(e.getMessage());
}
sysUserLogService.insert(sysUserLog);
}
}
}
// 发生异常时打印错误信息
if (e != null) {
StringBuffer sb = new StringBuffer();
sb.append("时间:");
sb.append(DateFormat.getDateTimeInstance().format(new Date()));
sb.append("方法:");
sb.append(joinPoint.getSignature() + "\n");
sb.append("异常信息:" + e.getMessage());
logger.error(sb.toString());
}
}
}
5.Controller请求处理层添加注解
/**
* 系统用户 列表页
*
* @param sysUserCriteria
* @param model
* @return
*/
@WebLog(channel = "web", name = "系统用户列表", action = "/sysUser", saveFlag = true)
@GetMapping("")
public String list(SysUserCriteria sysUserCriteria, Model model) {
model.addAttribute("sysUserCriteria", sysUserCriteria);
return "sysUser/list";
}
抛开杂念,静下心来,只看当下;
充电片刻,日积月累,步步向前。