说明:
日志我这里使用的是slf4j+log4j2,具体的使用方式可以参考《SpringBoot入门三,添加log4j2支持》与《SpringBoot配置多环境log4j2》;如果是使用其他日志组件,正常配置即可.
1.pom.xml添加引用
因为需要对web请求做切面来记录日志,所以需要入AOP模块
<!-- 引入aop切面支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.添加日志切面类
import java.util.Arrays;
import javax.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
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.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;
@Aspect // 将⼀个java类定义为切⾯类
@Component
public class WebLogAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
ThreadLocal<Long> startTime = new ThreadLocal<>();
/**
* <h5>功能:定义一个切入点,可以是一个规则表达式,也可以是一个注解</h5>
* 这里匹配com.qfx.modules.*.controller包下所有类的所有方法
*/
@Pointcut("execution(* com.qfx.modules.*.controller..*(..))")
public void webLog() {
}
// 根据需要在不同位置切入目标方法:
// @Before 前置通知(在目标方法执行之前执行)
// @After 后置通知(在目标方法执行之后执行,无论目标方法执行成功还是出现异常,都将执行后置方法)
// @AfterReturning 返回通知(在目标方法执行成功后才会执行,如果目标方法出现异常,则不执行;可以用来对返回值做一些加工处理)
// @AfterThrowing 异常通知(只在目标方法出现异常后才会执行,否则不执行;可以获取目标方法出现的异常信息)
// @Around 环绕通知(包含上面四种通知方法,环绕通知的功能最全面.环绕通知需要携带ProceedingJoinPoint类型的参数,且环绕通知必须有返回值,返回值即为目标方法的返回值)
/**
* <h5>功能:前置通知</h5>
*
* @param joinPoint
*/
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) {
startTime.set(System.currentTimeMillis());
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 记录下请求内容
logger.info("-------------------- 前置通知处理开始 --------------------");
logger.info("请求IP :{}", request.getRemoteAddr());
logger.info("请求信息:[{}] {}", request.getMethod(), request.getRequestURL().toString());
logger.info("调用方法:{} {}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
logger.info("接收参数:{}", Arrays.toString(joinPoint.getArgs()));
logger.info("-------------------- 前置通知处理结束 --------------------");
}
/**
* <h5>功能:后置通知</h5>
*
* @param returnInfo 特殊参数(returning):设定使用通知方法参数接收返回值的变量名
*/
@AfterReturning(pointcut = "webLog()", returning = "returnInfo")
public void doAfterReturning(Object returnInfo) {
logger.info("-------------------- 后置通知处理开始 --------------------");
// 处理完请求,返回内容
logger.info("返回信息:{}", returnInfo);
logger.info("耗时:{}秒", (System.currentTimeMillis() - startTime.get())/1000d);
logger.info("-------------------- 后置通知处理结束 --------------------");
}
/**
* <h5>功能:异常通知(一般不使用这个)</h5>
* 通过throwing属性指定目标方法出现的异常信息存储在e变量中,在异常通知方法中就可以从e变量中获取异常信息了
* @param point
* @param e 特殊参数(throwing):设定使用通知方法参数接收原始方法中抛出的异常对象名
*/
@AfterThrowing(pointcut = "webLog()", throwing = "e")
public void afterReturning(JoinPoint joinPoint, Exception e) {
logger.error("-------------------- 异常通知处理开始 --------------------");
logger.error("调用方法:{} {}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
logger.error("接收参数:{}", Arrays.toString(joinPoint.getArgs()));
logger.error("异常信息:", e);
logger.error("-------------------- 异常通知处理结束 --------------------");
}
}
3.添加业务类
import org.springframework.stereotype.Service;
@Service
public class TestService {
public String hello(String userName, int age) {
return userName + "你好,欢迎" + age + "的你来到java的世界!";
}
public String eat(String foodName) {
return "发现了一种名字叫[" + foodName + "]的新食物!";
}
}
4.添加控制类
4.1 控制类一
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.qfx.modules.test.service.TestService;
@RestController
@RequestMapping("test")
public class TestCtl {
@Autowired
TestService testService;
@RequestMapping("hello")
public String hello(String userName, int age) {
return testService.hello(userName, age);
}
@RequestMapping("helloTwo/{userName}")
public String helloTwo(@PathVariable("userName") String userName, int age) {
return testService.hello(userName, age);
}
}
4.2控制类二
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.qfx.modules.test.service.TestService;
@RestController
@RequestMapping("test2")
public class TestCtl2 {
@Autowired
TestService testService;
@RequestMapping("eat")
public String eat(String foodName) {
return "发现了一种名字叫[" + foodName + "]的新食物!";
}
@RequestMapping("calc")
public double calc(int a, int b) {
double c = a/b;
return c;
}
}
5.测试
5.1 正常请求
http://localhost/test/helloTwo/张三?age=16
http://localhost/test/hello?userName=李四&age=17
http://localhost/test2/eat?foodName=面包
5.2 异常请求
http://localhost/test2/calc?a=10&b=0
6.扩展知识
6.1 execution(切点函数)
除了返回类型模式、方法名模式和参数模式,其它项都是可选的.语法结构:
execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)
整个表达式可以分为五个部分:
1.execution():表达式主体.
2.第一个*号:表示返回类型,*号表示所有的类型.
3.包名:表示需要拦截的包名
4.第二个*号:表示类名, .*号表示当前包所有的类; ..*表示当前包及所有子包的所有的类
5.*(..) :第三个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数.
表达式
说明
execution(public * *(..))
匹配所有目标类的public方法.
第一个*代表返回类型,
第二个*代表方法名,
..代表任意入参的方法
execution(* *Add(..))
匹配目标类所有以Add为结尾的方法.第一个*代表返回类型,
*Add代表任意以Add为结尾的方法
execution(* com.qfx..*(..))
匹配com.qfx包及子孙包所有类所有方法
execution(* com.qfx.*.*(..))
匹配com.qfx包所有类所有方法
execution(* com.qfx.RoleSerImpl.*(..))
匹配com.qfx包RoleSerImpl类所有方法
execution(* com.qfx..*.Dao.find(..))
匹配com.qfx包下类名后缀为Dao,且方法名以find为前缀的方法
execution(* com.qfx.UserSer+.*(..))
匹配com.qfx包下实现UserSer接口的所有类的所有方法
execution(* info(String, int))
匹配info方法中,第一个参数是String,第二个int的方法
execution(* info(String, *)))
匹配info方法中,第一个参数是String,第二个任意类型
execution(* info(String, ..)))
匹配info方法中,第一个参数是String,后面任意参数
execution(* info(Object+)))
匹配info方法中,方法拥有一个入参,且入参是Object类型或该类的子类