0%

Spring AOP配置日志

首先讲一下SpringAOP , 在学过Spring之后 , 各位对Spring的两个要点肯定不陌生 , 一个是DI(依赖注入) , 一个就是AOP(切面编程)
那么AOP即切面编程 到底是用来做什么的呢 ?

我们先来看下下面这段代码:
没有使用aop的业务代码块

我们学Java面向对象的时候,如果代码重复了怎么办啊??可以分成下面几个步骤:

  • 抽取成方法
  • 抽取成类
    抽取成类的方式我们称之为:纵向抽取

但是,我们现在的办法不行:即使抽取成类还是会出现重复的代码,因为这些逻辑(开始、结束、提交事务)依附在我们业务类的方法逻辑中

现在纵向抽取的方式不行了,AOP的理念:就是将分散在各个业务逻辑代码中相同的代码通过横向切割的方式抽取到一个独立的模块中

横向抽取

上面的图也很清晰了,将重复性的逻辑代码横切出来其实很容易(我们简单可认为就是封装成一个类就好了),但我们要将这些被我们横切出来的逻辑代码融合到业务逻辑中,来完成和之前(没抽取前)一样的功能!这就是AOP首要解决的问题了!

AOP原理

AOP的实现原理也很简单 , 就是通过动态代理我们的业务逻辑类 , 来对我们业务逻辑的方法进行增强.

来源《精通Spring4.x 企业应用开发实战》一段话:

Spring AOP使用纯Java实现,它不需要专门的编译过程,也不需要特殊的类装载器,它在运行期通过代理方式向目标类织入增强代码。在Spring中可以无缝地将Spring AOP、IoC和AspectJ整合在一起。

来源《Spring 实战 (第4版)》一句话:

Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。

java中的想实现动态代理有两种方法 :

  1. JDK动态代理 (通过实现被代理类相同的接口完成)
  2. CGLib动态代理(通过继承被代理类完成)

JDK动态代理是需要实现某个接口了,而我们类未必全部会有接口,于是CGLib代理就有了

  • CGLib代理其生成的动态代理对象是目标类的子类
  • Spring AOP默认是使用JDK动态代理,如果代理的类没有接口则会使用CGLib代理。

看到这里我们就应该知道什么是Spring AOP(面向切面编程)了:将相同逻辑的重复代码横向抽取出来,使用动态代理技术将这些重复代码织入到目标对象方法中,实现和原来一样的功能。

这样一来,我们就在写业务时只关心业务代码,而不用关心与业务无关的代码.

那么介绍到这里也算把AOP的核心理念给讲述了一遍
这里给到一个我在最近做的一个后台管理系统时 , 用AOP完成日志处理的一些过程和想法

使用AOP来完成日志处理

这个日志主要用来收集 , 系统在何时被哪个用户访问和访问的路径 , 方法 等等

创建日志实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public clss SysLog{
private String id; // 日志 id
private Date visitTime; // 访问时间
private String visitTimeStr;
private String username; // 访问用户 用户名
private String ip; // 访问用户 id
private String url; // 访问路径
private Long executionTime; // 执行访问操作时间
private String method; // 执行方法

/**
* getter setter 这里省略
*/
}

创建对应的数据库

这里把对应的日志我们存入到数据库中, 并没用日志文件来存储 , 数据库用的oracle(目前正在学习这个)

1
2
3
4
5
6
7
8
9
10
-- 日志
CREATE TABLE sysLog(
id VARCHAR2(32) default SYS_GUID() PRIMARY KEY, -- id 主键 默认随机uuid
visitTime timestamp, -- 访问时间
username VARCHAR2(50), -- 访问用户
ip VARCHAR2(30), -- 访问用户ip地址
url VARCHAR2(50), -- 访问路径
executionTime int, -- 执行时间
method VARCHAR2(200) -- 访问方法
)

创建切面类

注意要在spring配置文件中开启对日志包的扫描

1
2
3
4
5
6
// 让springioc容器可以扫描到该类
@Component
// 用于声明当前类为一个切面类
@Aspect
public class SysLogAOP {
}

我们主要是对Controller层的访问进行日志处理(访问路径获取)

由于我们在定义日志时要获取访问时间和方法执行时间 , 那么我们就需要给给切入点配置两个通知 :

  • 一个前置通知 , 用于获取方法访问时间和访问方法
  • 一个后置通知 , 用于获取剩下的日志数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Component
@Aspect
public class SysLogAOP {

/**
* 配置前置通知 , 对所有的controller中的方法进行增强
* 用于获取切入时间(方法执行时间), 执行方法的类名, 执行的方法
* @param jp 方法切入点对象(可以理解为封装切点方法后的对象)
*/
@Before("execution(* com.weison.controller.*.*(..))")
public void dobefore(JoinPoint jp) {

}

/**
* 配置后置通知 , 对所有的controller中的方法进行增强
* 用于获取方法执行时间(方法执行耗费时间), 访问用户, 访问用户的ip
* @param jp 方法切入点对象(可以理解为封装切点方法后的对象)
*/
@After("execution(* com.weison.controller.*.*(..))")
public void doafter(JoinPoint jp){

}

}

由于我们要获取执行时间是在第二个方法 , 也就是后置通知中配置 , 而访问时间是在前置通知中获取 , 那么这里我们需要定义一个成员变量Date startTime
其实也可以再定义两个成员变量Class executClassMehtod executMethod , 用于后置通知中日志实体对象的属性封装.

首先 , 我们在前置通知中 直接 给 startTime 赋值(获取到访问时间) , 然后通过通知方法JoinPoint的实现类来获取到我们执行类和执行方法名还有执行方法的参数

1
2
3
4
5
6
7
8
9
10
11
@Before("execution(* com.weison.controller.*.*(..))")
public void dobefore(JoinPoint jp) {
// 获取执行时间
starttime = new Date();
// 获取执行的方法的类
executclass = jp.getTarget().getClass();
// 获取执行的方法名
String methodname = jp.getSignature().getName();
// 获取执行方法的参数
Object[] args = jp.getArgs();
}

然后我们先判断参数数组是否为空 , 为空则可以直接通过反射来获取执行方法()

1
2
3
4
5
// 获取到的参数集合为null或者数组长度为0则为无参方法
if (args == null || args.length == 0){
// 注意 , 该方法只能获取到无参的方法
executmethod = executclass.getMethod(methodname);
}

如果数组不为空且长度不为零 , 则我们可以创建一个Class数组来存放这些参数的类对象

1
2
3
4
5
6
7
8
if (args != null && args.length > 0){ // 表示执行方法有参数 需要 封装参数
// 创建类数组来存储参数
Class[] argsclass = new Class[args.length];
// 循环写进参数
for (int i = 0; i < argsclass.length; i++) {
argsclass[i] = args[i].getClass();
}
}

我们可以点进getMethod方法 , 可以看到这个方法是个不定长参数的方法 , 除了传入methodname还可以传入Class对象和Class[]数组

所以我们可以在上面的判断体中接着写入 :

1
executmethod = executclass.getMethod(methodname,argsclass);

到这里前置通知已经弄完了 , 我们已经获取到了访问时间和执行方法

接下来我们配置后置通知

后置通知是在切入点执行完成后执行的 , 所以我们第一个就可以把执行时间给获取到

1
2
3
4
5
@After("execution(* com.weison.controller.*.*(..))")
public void doafter(JoinPoint jp){
// 获取执行时间
long executtime = new Date().getTime() - starttime.getTime();
}

然后是获取url , 我项目用的是springmvc来完成的controller的展示和路径分发的 , 所以这里我们获取路径的就相对简单了很多
项目使用了requestMapping注解来来配置路径 , 所以我们这里就可以通过反射来获取注解对象在通过类型转换获取到注解的属性值:

1
2
3
4
5
6
7
8
9
10
11
// 获取访问的url
// 先声明一个空字符串来拼接
StringBuilder url = new StringBuilder("");
RequestMapping classurl = (RequestMapping) executclass.getAnnotation(RequestMapping.class);
if (classurl != null) {
url.append(classurl.value()[0]);
}
RequestMapping methodurl = (RequestMapping) executmethod.getAnnotation(RequestMapping.class);
if (methodurl != null) {
url.append(methodurl.value()[0]);
}

然后是获取ip地址 , 这个就相对简单一点 , 我们在web.xml文件中配置一个监听器就可以了 , 这个监听器是spring框架集成的 , 专门用于监听request域对象的产生和销毁的

1
2
3
4
<!-- 配置监听器,监听request域对象的创建和销毁的 -->
<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>

然后我们在切面类中声明一个成员变量 , 然后使用@AutoWired注解自动注入就可以了

1
2
3
// web.xml配置监听器后自动生成的 , 我们可以直接注入
@Autowired
private HttpServletRequest request;

然后我们获取下访问用户的ip地址 :

1
2
// 获取ip地址
String ip = request.getRemoteAddr();

到这里我们还差访问用户就算把日志内容给获取完整了 , 如果是直接把用户存入session中的 , 就通过session来获取即可 , 我当前写的项目使用的是Spring Security来完成用户的登录验证的 , 所以这里我需要获取到SecurityContext对象来获取User对象

1
2
3
4
5
6
// 获取到SecurityContext 对象
SecurityContext context = SecurityContextHolder.getContext();
// 获取到当前的操作对象(可以点进方法可以看到context对象中存储的这个对象是歌user对象, 这里我就不做判断直接强转)
User user = (User) context.getAuthentication().getPrincipal();
// 获取用户名
String username = user.getUsername();

然后我们封装下这些日志信息到实体对象中:

1
2
3
4
5
6
7
8
SysLog sysLog = new SysLog();
// 封装日志信息
sysLog.setExecutionTime(executtime);
sysLog.setIp(ip);
sysLog.setMethod("[调用类] " + executclass.getName() + "\n[执行方法] " + executmethod.getName());
sysLog.setUrl(url);
sysLog.setUsername(username);
sysLog.setVisitTime(starttime);

调用SysLogservice业务处理层把sysLog对象存进数据库

1
2
3
4
5
@Autowired
private SysLogservice sysLogservice;

// 存储数据
sysLogservice.save(sysLog);

到这里 , 这个基于切面的日志处理就算完成了 , 为了便于查看日志访问信息 , 顺便在后台管理里写了一个页面用于查看 :

日志查看

源码我已经上传到了github, 有需要的去看就好了 , 点我跳转到仓库;