在开始今天的主题之前,我们先对Spring的AOP进行简单的回顾:
AOP:全称是Aspect Oriented Programming 即面向切面编程
简单来说就是我们把程序中重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,来对已有方法进行增强
作用:
程序运行期间,不修改源码对已有方法进行增强
优势:
减少重复代码
提高开发效率
维护方便
使用动态代理技术
切面:由切点和增强(引介)组成,它既包括横切逻辑的定义,也包括连接点的定义
切入点:匹配连接点的断言(规则)。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行 切入点表达式:execution([修饰符]
返回类型 [声明类型].方法名(参数) [异常])
接下来进行我们今天的主题,使用Spring的AOP记录用户的操作,并且将操作记录写入数据库,在下面的文章中,会结合代码的方式来一步步的实现功能
1、搭建环境,创建一个springboot的工程,并且引入相关依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.lzycug</groupId>
<artifactId>aop</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>aop</name>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、在application.yaml配置文件中,配置端口、数据源信息(数据库使用mysql,实际根据自己情况进行配置)
server:
port: 9090
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://47.98.xxx.xx:3306/blog?useUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
3、该案例中,我们准备记录如下的操作信息:访问时间、访问时长、操作者的IP、操作的URL(请求地址)、访问的方法名称等信息,根据如上信息创建日志记录的实体类
package com.lzycug.aop.pojo;
import lombok.Data;
import java.util.Date;
/**
* description:日志记录的实体类
* author:lzyCug
* date: 2020/4/5 22:37
*/
@Data
public class SysLog {
// 访问记录ID
private String id;
// 访问时间
private Date visitTime;
// 访问的IP
private String ip;
// 访问的地址
private String url;
// 访问耗时(毫秒)
private Long executionTime;
// 访问的方法名称
private String method;
}
4、在mysql数据库中创建操作日志记录表
/*
SQLyog Ultimate v12.09 (64 bit)
MySQL - 5.6.22 : Database - blog
*********************************************************************
*/
/*!40101 SET NAMES utf8 */;
/*!40101 SET SQL_MODE=''*/;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`blog` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `blog`;
/*Table structure for table `sys_log` */
DROP TABLE IF EXISTS `sys_log`;
CREATE TABLE `sys_log` (
`id` int(20) NOT NULL AUTO_INCREMENT COMMENT '日志记录主键',
`visit_time` datetime DEFAULT NULL COMMENT '访问时间',
`ip` varchar(64) DEFAULT NULL COMMENT '访问的IP',
`url` varchar(20) DEFAULT NULL COMMENT '访问的地址',
`execution_time` bigint(20) DEFAULT NULL COMMENT '访问耗时(毫秒)',
`method` varchar(100) DEFAULT NULL COMMENT '访问的方法名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
5、定义切面类,用于操作日志记录
package com.lzycug.aop.config;
import com.lzycug.aop.pojo.SysLog;
import com.lzycug.aop.service.LogService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.Objects;
import javax.servlet.http.HttpServletRequest;
/**
* description:切面类,用于记录操作
* author:lzyCug
* date: 2020/4/5 22:34
*/
@Component
@Aspect
public class LogAspect {
@Autowired
LogService logService;
@Autowired
HttpServletRequest httpServletRequest;
private Date visitTime; // 访问时间
private Class aClass; // 访问的类
private Method method; // 访问的方法
// 方式一:直接在通知上使用切入点表达式,指定需要记录操作的类或者方法
@Before("execution(* com.lzycug.aop.controller.*.*(..))")
public void doBefore(JoinPoint joinPoint) throws NoSuchMethodException {
// 访问时间
visitTime = new Date();
// 获取访问的类
aClass = joinPoint.getTarget().getClass();
// 获取访问的方法名称
String methodName = joinPoint.getSignature().getName();
// 获取访问方法的参数数组
Object[] args = joinPoint.getArgs();
if (ObjectUtils.isEmpty(args)) {
// 获取无参方法
method = aClass.getMethod(methodName);
} else {
// 获取有参数的方法
Class[] classes = new Class[args.length];
for (int i = 0; i < args.length; i++) {
classes[i] = args[i].getClass();
}
method = aClass.getMethod(methodName, classes);
}
}
@After("execution(* com.lzycug.aop.controller.*.*(..))")
public void doAfter(JoinPoint joinPoint) {
// 获取访问的时长
long time = new Date().getTime() - visitTime.getTime();
// 获取访问的IP
String ip = httpServletRequest.getRemoteAddr();
// 获取访问的url
if (ObjectUtils.isEmpty(aClass) || ObjectUtils.isEmpty(method) || Objects.equals(aClass, LogAspect.class)) {
return;
}
// 获取类上的映射路径
Annotation annotation = aClass.getAnnotation(RequestMapping.class);
RequestMapping requestMapping = null;
if (annotation instanceof RequestMapping) {
requestMapping = (RequestMapping) annotation;
}
if (ObjectUtils.isEmpty(requestMapping)) {
return;
}
String[] classUrl = requestMapping.value();
RequestMapping methodAnnotation = method.getAnnotation(RequestMapping.class);
if (ObjectUtils.isEmpty(methodAnnotation)) {
return;
}
String[] methodUrl = methodAnnotation.value();
String url = classUrl[0] + "/" + methodUrl[0];
// 封装日志记录对象
SysLog sysLog = new SysLog();
sysLog.setVisitTime(visitTime);
sysLog.setIp(ip);
sysLog.setUrl(url);
sysLog.setExecutionTime(time);
sysLog.setMethod("[类名] " + aClass.getName() + "[方法名] " + method.getName());
logService.insert(sysLog);
}
}
代码说明:
@Component :将该切面类交由spring管理
@Aspect:将该类声明为一个切面类
@Autowired:注入依赖
@Before: 前置通知, 在方法执行之前执行
@After: 后置通知, 在方法执行之后执行
@AfterReturning: 返回通知, 在方法返回结果之后执行
@AfterThrowing: 异常通知, 在方法抛出异常之后
@Around: 环绕通知, 围绕着方法执行
操作者IP的获取通过注入HttpServletRequest 对象,使用getRemoteAddr()方法即可获取访问IP
访问时间通过在前置通知中,直接获取当前时间获取
访问时长通过在后置通知中获取当前时间和访问时间的差值确定
访问的URL通过JoinPoint对象的joinPoint.getTarget().getClass()方法获取访问的类,joinPoint.getSignature().getName()获取方法名,再通过反射方式获取Method对象,在通过Class对象及Method对象获取上面的@RequestMapping注解的value值,拼装即可得到URL的值
访问的方法通过Method的getName()方法获取
6、再通过编写日志记录的业务层及数据操作层,来达到写入数据库的目的,此处不赘述,案例代码完整版会在文章最后附上GitHub地址
7、实现方式的多样化一
// 方式二:使用切入点表达式注解的方式,在需要记录操作的的地方使用方法名pt1()
@Pointcut("execution(* com.lzycug.aop.controller.*.*(..))")
public void pt1() {
}
@Before("pt1()") // 注意括号的添加
public void doBefore(JoinPoint joinPoint) throws NoSuchMethodException {
... ...
}
8、实现方式多样化二
package com.lzycug.aop.config;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* description:自定义注解类,在需要记录操作的方法或者类上添加该注解
*
* @Target({ElementType.METHOD,ElementType.TYPE}) 表示这个注解可以用用在类/接口上,还可以用在方法上
* @Retention(RetentionPolicy.RUNTIME) 表示这是一个运行时注解,即运行起来之后,才获取注解中的相关信息,而不像基本注解如@Override 那种不用运行,在编译时就可以进行相关工作的编译时注解。
* @Inherited 表示这个注解可以被子类继承
* @Documented 表示当执行javadoc的时候,本注解会生成相关文档
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface LogAop {
}
// 方式三:切入点表达式注解方式,在使用自定义注解的地方执行使用了方法pt2()的具体通知
@Pointcut("@annotation(com.lzycug.aop.config.LogAop)")
public void pt2() {
}
// 方式一:直接在通知上使用切入点表达式,指定需要记录操作的类或者方法
@Before("pt2()") // 注意括号添加
public void doBefore(JoinPoint joinPoint) throws NoSuchMethodException {
... ...
}
package com.lzycug.aop.controller;
import com.lzycug.aop.config.LogAop;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* description:用于测试的访问控制层
* author:lzyCug
* date: 2020/4/5 23:00
*/
@RestController
@RequestMapping("/aop")
public class AopController {
@LogAop
@RequestMapping("request")
public void testRequest() {
System.out.println("request...");
}
}
完整的案例代码github地址:https://github.com/Lzycug/blog.git