您的位置:首页 > 编程语言 > Java开发

46. Spring Boot中使用AOP统一处理Web请求日志【从零开始学Spring Boot】

2016-08-26 08:53 1261 查看


 

【视频&交流平台】

à SpringBoot视频
http://study.163.com/course/introduction.htm?courseId=1004329008&utm_campaign=commission&utm_source=400000000155061&utm_medium=share
à SpringCloud视频
http://study.163.com/course/introduction.htm?courseId=1004638001&utm_campaign=commission&utm_source=400000000155061&utm_medium=share
à Spring
Boot源码
https://gitee.com/happyangellxq520/spring-boot
à Spring
Boot交流平台
http://412887952-qq-com.iteye.com/blog/2321532
 

 
 

在之前一系列的文章中都是提供了全部的代码,在之后的文章中就提供核心的代码进行讲解。有什么问题大家可以给我留言或者加我QQ,进行咨询。

       AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是Spring框架中的一个重要内容,它通过对既有程序定义一个切入点,然后在其前后切入不同的执行内容,比如常见的有:打开数据库连接/关闭数据库连接、打开事务/关闭事务、记录日志等。基于AOP不会破坏原来程序逻辑,因此它可以很好的对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

       本文就是要通过AOP技术统一处理web请求的日志。

准备工作

       因为需要对web请求做切面来记录日志,所以先引入web模块,并创建一个简单的hello请求的处理。

<dependency>
        
<groupId>org.springframework.boot</groupId>
        
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
 

实现一个简单请求处理:

@RestController
publicclass HelloController {
      
      
@RequestMapping("/hello")
       public Stringhello(Stringname,intstate){
              return"name"+name+"---"+state;
       }
}
这时候我们编写一个启动类启动运行程序访问:

http://127.0.0.1:8080/hello?name=林峰&state=1 就能看到页面返回的信息了,但是现在还是不能进行拦截的。

 

引入AOP依赖

<dependency>
        
<groupId>org.springframework.boot</groupId>
        
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
 

在完成了引入AOP依赖包后,一般来说并不需要去做其他配置。也许在Spring中使用过注解配置方式的人会问是否需要在程序主类中增加@EnableAspectJAutoProxy来启用,实际并不需要。

      

可以看下面关于AOP的默认配置属性,其中spring.aop.auto属性默认是开启的,也就是说只要引入了AOP依赖后,默认已经增加了@EnableAspectJAutoProxy。

# AOPspring.aop.auto=true # Add @EnableAspectJAutoProxy.
spring.aop.proxy-target-class=false# Whether subclass-based (CGLIB) proxies are to be created (true) as opposed tostandard Java interface-based proxies (false).
我在做测试的时候,以上两个配置我都没有进行使用,请自行进行测试。

      而当我们需要使用CGLIB来实现AOP的时候,需要配置
spring.aop.proxy-target-class=true,不然默认使用的是标准Java的实现。

 

实现Web层的日志切面

实现AOP的切面主要有以下几个要素:
·        使用@Aspect注解将一个java类定义为切面类
·        使用@Pointcut定义一个切入点,可以是一个规则表达式,比如下例中某个package下的所有函数,也可以是一个注解等。
·        根据需要在切入点不同位置的切入内容
o   使用@Before在切入点开始处切入内容
o   使用@After在切入点结尾处切入内容
o   使用@AfterReturning在切入点return内容之后切入内容(可以用来对处理返回值做一些加工处理)
o   使用@Around在切入点前后切入内容,并自己控制何时执行切入点自身的内容
o   使用@AfterThrowing用来处理当切入内容部分抛出异常之后的处理逻辑。
o    
package com.kfit.config.aop.log;
import java.util.Arrays;
import java.util.Enumeration;
importjavax.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
importorg.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
importorg.springframework.core.annotation.Order;
importorg.springframework.stereotype.Component;
importorg.springframework.web.context.request.RequestContextHolder;
importorg.springframework.web.context.request.ServletRequestAttributes;
 
/**
 *实现Web层的日志切面
 *@author Angel(QQ:412887952)
 *@version v.0.1
 */
@Aspect
@Component
@Order(-5)
publicclass WebLogAspect {
       private Loggerlogger =  LoggerFactory.getLogger(this.getClass());
      
      
/**
        *定义一个切入点.
        *解释下:
        *
        * ~第一个 *代表任意修饰符及任意返回值.
        * ~第二个 *任意包名
        * ~第三个 *代表任意方法.

        * ~第四个 *定义在web包或者子包
        * ~第五个 *任意方法
        * ~ ..匹配任意数量的参数.
        */
        @Pointcut("execution(public * com.kfit.*.web..*.*(..))")
        publicvoid webLog(){}
        
        @Before("webLog()")
        publicvoid doBefore(JoinPointjoinPoint){
               
             
// 接收到请求,记录请求内容
               logger.info("WebLogAspect.doBefore()");
               ServletRequestAttributesattributes =(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequestrequest =attributes.getRequest();
           
           
        
// 记录下请求内容
       
logger.info("URL : " +
request.getRequestURL().toString());
       
logger.info("HTTP_METHOD : " +
request.getMethod());
       
logger.info("IP : " +
request.getRemoteAddr());
       
logger.info("CLASS_METHOD : " +
joinPoint.getSignature().getDeclaringTypeName()+
"." + joinPoint.getSignature().getName());
       
logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
      //获取所有参数方法一:
        Enumeration<String>
enu=request.getParameterNames(); 
        while(enu.hasMoreElements()){ 
               StringparaName=(String)enu.nextElement(); 
               System.out.println(paraName+": "+request.getParameter(paraName)); 
        } 
        }
        
        @AfterReturning("webLog()")
        publicvoid doAfterReturning(JoinPointjoinPoint){
             
// 处理完请求,返回内容
               logger.info("WebLogAspect.doAfterReturning()");
        }
        
}
整个代码比较不好理解地方就是切点表达式,已经在注释中详细说明了,这里不再过多的介绍。编码中需要根据您自己的包命名规范进行修改下。

这时候运行程序访问 http://127.0.0.1:8080/hello?name=林峰&state=1  就可以看到控制台的打印信息了。

: WebLogAspect.doBefore()
: URL : http://127.0.0.1:8080/hello : HTTP_METHOD : GET
: IP : 127.0.0.1
: CLASS_METHOD : com.kfit.demo.web.HelloController.hello
: ARGS : [林峰, 1]
name: 林峰
state: 1
: WebLogAspect.doAfterReturning()
: 耗时(毫秒) : 1

优化:AOP切面中的同步问题

       在WebLogAspect切面中,分别通过doBefore和doAfterReturning两个独立函数实现了切点头部和切点返回后执行的内容,若我们想统计请求的处理时间,就需要在doBefore处记录时间,并在doAfterReturning处通过当前时间与开始处记录的时间计算得到请求处理的消耗时间。

 

       那么我们是否可以在WebLogAspect切面中定义一个成员变量来给doBefore和doAfterReturning一起访问呢?是否会有同步问题呢?

 

       的确,直接在这里定义基本类型会有同步问题,所以我们可以引入ThreadLocal对象,像下面这样进行记录改造代码为如下:

package com.kfit.config.aop.log;
import java.util.Arrays;
import java.util.Enumeration;
importjavax.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
importorg.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
importorg.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
importorg.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
importorg.springframework.web.context.request.ServletRequestAttributes;
 
/**
 *实现Web层的日志切面
 *@author Angel(QQ:412887952)
 *@version v.0.1
 */
@Aspect
@Component
 
publicclass WebLogAspect {
       private Loggerlogger =  LoggerFactory.getLogger(this.getClass());
      
       ThreadLocal<Long>startTime =newThreadLocal<Long>();
      
      
/**
        *定义一个切入点.
        *解释下:
        *
        * ~第一个 *代表任意修饰符及任意返回值.
        * ~第二个 *任意包名
        * ~第三个 *代表任意方法.

        * ~第四个 *定义在web包或者子包
        * ~第五个 *任意方法
        * ~ ..匹配任意数量的参数.
        */
        @Pointcut("execution(public * com.kfit.*.web..*.*(..))")
        publicvoid webLog(){}
        
        @Before("webLog()")
        publicvoid doBefore(JoinPointjoinPoint){
               startTime.set(System.currentTimeMillis());
               
             
// 接收到请求,记录请求内容
               logger.info("WebLogAspect.doBefore()");
               ServletRequestAttributesattributes =(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequestrequest =attributes.getRequest();
           
           
        
// 记录下请求内容
       
logger.info("URL : " +
request.getRequestURL().toString());
       
logger.info("HTTP_METHOD : " +
request.getMethod());
       
logger.info("IP : " +
request.getRemoteAddr());
       
logger.info("CLASS_METHOD : " +
joinPoint.getSignature().getDeclaringTypeName()+
"." + joinPoint.getSignature().getName());
       
logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
      //获取所有参数方法一:
        Enumeration<String>
enu=request.getParameterNames(); 
        while(enu.hasMoreElements()){ 
               StringparaName=(String)enu.nextElement(); 
               System.out.println(paraName+": "+request.getParameter(paraName)); 
        } 
        }
        
        @AfterReturning("webLog()")
        publicvoid doAfterReturning(JoinPointjoinPoint){
             
// 处理完请求,返回内容
               logger.info("WebLogAspect.doAfterReturning()");
               logger.info("耗时(毫秒) : " + (System.currentTimeMillis()-startTime.get()));
        }
        
}
 

优化:AOP切面的优先级

     由于通过AOP实现,程序得到了很好的解耦,但是也会带来一些问题,比如:我们可能会对Web层做多个切面,校验用户,校验头信息等等,这个时候经常会碰到切面的处理顺序问题。

所以,我们需要定义每个切面的优先级,我们需要@Order(i)注解来标识切面的优先级。i的值越小,优先级越高。假设我们还有一个切面是CheckNameAspect用来校验name必须为didi,我们为其设置@Order(10),而上文中WebLogAspect设置为@Order(5),所以WebLogAspect有更高的优先级,这个时候执行顺序是这样的:
·        在@Before中优先执行@Order(5)的内容,再执行@Order(10)的内容
·        在@After和@AfterReturning中优先执行@Order(10)的内容,再执行@Order(5)的内容
所以我们可以这样子总结:
>在切入点前的操作,按order的值由小到大执行
>在切入点后的操作,按order的值由大到小执行
在实际中order值可以设置为负值,确保是第一个进行执行的。

【Spring Boot 系列视频】

视频&交流平台:

à Spring Boot网易云课堂视频
http://study.163.com/course/introduction.htm?courseId=1004329008
à Spring Boot交流平台
http://412887952-qq-com.iteye.com/blog/2321532
 

网易云课堂视频最新更新

第十一章 Spring Boot 日志

1、spring boot日志—理论

2、Spring Boot日志-logback

3、Spring Boot日志-log4j2

第十二章 Spring Boot 知识点2

1、spring boot 服务配置和部署

2、Spring Boot 定制URL匹配规则

 
 
历史章节:

 

第一章 快速开始

1、Spring Boot之Hello World

2、Spring Boot之Hello World访问404

 

第二章 Spring Boot之JSON

1、spring boot返回json数据

2、Spring Boot完美使用FastJson解析JSON数据

 

第三章 Spring Boot热部署

1、Spring Boot热部署(springloader)

2、springboot + devtools(热部署)

 

第四章 Spring Boot数据库

1、Spring Boot JPA/Hibernate/Spring Data概念

2、Spring Boot JPA-Hibernate

3、Spring Boot Spring Data JPA介绍

4、Spring Boot JdbcTemplate

5、Spring Boot集成MyBatis

 

第五章 web开发

1、全局异常捕捉

2、配置server信息

3、spring boot使用thymeleaf

4、Spring Boot 使用freemarker

5、Spring Boot添加JSP支持

 

第六章 定时任务

1、Spring Boot定时任务

2、Spring Boot 定时任务升级篇(动态修改cron参数)

3、Spring Boot 定时任务升级篇(动态添加修改删除定时任务)

4、Spring Boot 定时任务升级篇(集群/分布式下的定时任务说明)

5、Spring Boot Quartz介绍

6、Spring Boot Quartz在Java Project中使用

7、Spring Boot 集成Quartz普通使用

8、Spring Boot 集成Quartz升级版

9、Spring Boot 集成Quartz二次升级版

10、Spring Boot 集成Quartz-Job如何自动注入Spring容器托管的对象

 

第七章 Spring Boot MyBatis升级篇

1、Spring Boot MyBatis升级篇-注解

2、Spring Boot MyBatis升级篇-注解-自增ID

3、Spring Boot MyBatis升级篇-注解-增删改查

4、Spring Boot MyBatis升级篇-注解-分页查询

5、Spring Boot MyBatis升级篇-注解-分页PageHelper不生效

6、Spring Boot MyBatis升级篇-注解- mybatic insert异常:BindingException: Parameter 'name' not found

7、Spring Boot MyBatis升级篇-注解- #和$符号特别篇

8、Spring Boot MyBatis升级篇-注解-@Result

9、Spring Boot MyBatis升级篇-注解-动态SQL(if test)-方案一:<script>

10、Spring Boot MyBatis升级篇-注解-动态SQL(if test)-方案二:@Provider

11、Spring Boot MyBatis升级篇-注解-动态SQL-参数问题

12、Spring Boot MyBatis升级篇-注解-特别篇:@MapperScan和@Mapper

13、Spring Boot MyBatis升级篇-XML

14、Spring Boot MyBatis升级篇-XML-自增ID

15、Spring Boot MyBatis升级篇-XML-增删改查

16、Spring Boot MyBatis升级篇-XML-分页查询

17、Spring Boot MyBatis升级篇-XML-分页PageHelper不生效

18、Spring Boot MyBatis升级篇-XML-动态SQL(if test)

19、Spring Boot MyBatis升级篇-XML-注解-初尝试

20、Spring Boot MyBatis升级篇- pagehelper替换为pagehelper-spring-boot-starter

 

第八章 Spring Boot 知识点1

1、Spring Boot 拦截器HandlerInterceptor

2、Spring Boot启动加载数据CommandLineRunner

3、Spring Boot环境变量读取和属性对象的绑定

4、Spring Boot使用自定义的properties

5、Spring Boot使用自定义的properties

6、Spring Boot使用@SpringBootApplication

7、Spring Boot 监控和管理生产环境

 

第十章 Spring Boot 打包部署

1、Spring Boot打包部署((提供Linux的sh文件))

 

第十一章 Spring Boot 日志

1、spring boot日志—理论

2、Spring Boot日志-logback

 
3、Spring Boot日志-log4j2
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息