一、Spring AOP(概念)
Aspect Oriented Programming:面向切面编程,是OOP(面向对象编程的补充和完善)。
(1)什么时候会出现面向切面编程的需求?
按照软件重构的思想,如果多个类中出现重复的代码,就该考虑定义一个共同的抽象类,将这些共有的代码提取到抽象类中,我们称为纵向抽取。但当我们想要给所有类方法添加性能检测、事务控制或者存取用户操作日志时,Aop将这些分散在各个业务逻辑代码中的相同代码,通过横向切割的方式抽取到一个独立模块中。
(2)解决的主要问题:
将这些独立的逻辑融合到业务逻辑中,完成跟原来一样的业务逻辑。
(3)Aop术语
- 切面(Aspect)
切面是一个横切关注点的模块化,一个切面能够包含同一个类型的不同增强方法,比如说事务处理和日志处理可以理解为两个切面。切面由切入点和通知组成,它既包含了横切逻辑的定义,也包括了切入点的定义。 Spring AOP就是将切面所定义的横切逻辑织入到切面所指定的连接点中。
可以简单理解为,含有@Aspect注解的类就说切面
- 目标对象(Target)
指的是要被增强的对象,即包含主业务逻辑的类对象。或者说是被一个或多个切面所通知的对象。
- 连接点(JoinPoint)
连接点就是被拦截到的程序执行点,因为Spring只支持方法类型的连接点,所以在Spring中连接点就是被拦截到的方法。
- 切入点(PointCut)
切入点是对连接点进行拦截的条件定义。切入点表达式如何和连接点匹配是AOP的核心,Spring缺省使用AspectJ切入点语法。
一般认为,所有的方法都可以认为是连接点,但是我们并不希望在所有的方法上都添加通知,而切入点的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述) 来匹配连接点,给满足规则的连接点添加通知。
- 通知(Advice)
通知是指拦截到连接点之后要执行的代码。通知分为以下几种:
- 前置通知(before):在执行业务代码前做的操作,比如获取连接对象
- 后置通知(after):在执行业务代码后做的操作,比如关闭连接对象
- 异常通知(afterThrowing):执行业务代码后出现异常,需要做的操作,比如回滚事务
- 返回通知(afterReturning):在执行业务代码后无异常,会执行的操作
- 环绕通知(around)
- 织入(Weaving)
- 增强器(Adviser)
二、日志记录的实现(实践)
基于Springboot框架的项目需求如下:
1.添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.创建一个自定义的注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
String value() default "";
}
3.添加日志切面
/**
* 切点类
*/
@Aspect
@Component
public class SystemLogAspect {
//注入Service用于把日志保存数据库
@Resource
private LogService logServiceImp;
//本地异常日志记录对象
private static final Logger logger = LoggerFactory.getLogger(SystemLogAspect. class);
//Controller层切点
@Pointcut("@annotation(com.taiji.sms.foundation.annotation.SysLog)")
public void controllerAspect() {
}
/**
* 前置通知 用于拦截Controller层记录用户的操作
* @param joinPoint 切点
*/
@Before("controllerAspect()")
public void doBefore(JoinPoint joinPoint) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//读取session中的用户
UserInfoEntity user=(UserInfoEntity)HttpContextUtils.getValueFromHttpSession("UserInfoEntity");
//请求的IP
//String ip = request.getRemoteAddr();
String requestURI=request.getRequestURI();
String ip=WebUtils.getRemoteAddr(request);
String method = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
String params = "";
if (joinPoint.getArgs() != null && joinPoint.getArgs().length > 0) {
for ( int i = 0; i < joinPoint.getArgs().length; i++) {
params+=JsonUtils.objectToJson(joinPoint.getArgs()[i])+";";
}
}
try {
//*========控制台输出=========*//
String operation=getControllerMethodDescription(joinPoint);
String username=user.getUsername();
TbLogEntity log=new TbLogEntity();
log.setCreateTime(MyUtil.getNowDateStr2());
log.setIp(ip);
log.setOperation(operation);
log.setParams(params);
log.setUsername(username);
log.setMethod(requestURI);
//*========保存数据库日志=========*//
logServiceImp.insLog(log);
//保存数据库
} catch (Exception e) {
//记录本地异常日志
logger.error("前置通知异常,异常信息:{}", e.getMessage(), e);
}
}
/**
* 获取注解中对方法的描述信息 用于Controller层注解
*
* @param joinPoint 切点
* @return 方法描述
* @throws Exception
*/
public static String getControllerMethodDescription(JoinPoint joinPoint) throws Exception {
String targetName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] arguments = joinPoint.getArgs();
Class targetClass = Class.forName(targetName);
Method[] methods = targetClass.getMethods();
String description = "";
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Class[] clazzs = method.getParameterTypes();
if (clazzs.length == arguments.length) {
description = method.getAnnotation(SysLog.class).value();
break;
}
}
}
return description;
}
}
4.方法调用
/**
* 退出
*/
@RequestMapping("/logout")
@SysLog(value = "用户退出登录")
public ModelAndView logout(HttpServletRequest request){
ModelAndView mView=new ModelAndView();
mView.setViewName("logout");
return mView;
}
关于记录用户操作日志,对注解自定义的理解也是很重要的,默默打开书去复习注解啦,bye~~~~❤