springboot用aop做日志

说明:在每一个接口访问的时候记录访问日志,用户的信息,参数信息,执行时间,执行时长和返回的数据

在ssm框架中我们都知道用注解做aop只需要在配置文件中增加一行配置即可,如下:<aop:aspectj-autoproxy />

但在springboot中做aop只要引进依赖,默认spring.aop.auto=ture,也就是说相当于在启动类中添加了@EnableAspectJAutoProxy注解

当我们要做CGLIB来实现AOP的时候,需要增加配置spring.aop.proxy-target-class=true,不然是java默认的标准来实现

1.添加依赖

<!--spring切面aop依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

2.创建一个类来实现aop的功能

package com.jsc.log.aopconfig;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import com.jsc.log.service.OpDatabaseService;
import com.jsc.model.AdminUser;
import com.jsc.log.pojo.LogErrorlog;
import com.jsc.log.pojo.LogOperVO;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.alibaba.fastjson.JSONObject;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Aspect
@Component 
public class LogAOP {
	private RequestAttributes requestAttributes;
	private static ThreadLocal<Long> currenttime = new ThreadLocal<Long>();
	private static ThreadLocal<String> userId = new ThreadLocal<String>();
	@Autowired
	private OpDatabaseService opDatabaseService;

	@Pointcut("execution(* com.jsc.*.controller..*.*(..))")//切入点描述,这个是Controller包的切入点
	public void controllerLog(){}

    /**
     * 前置通知
     * @param joinPoint
     */
	@Before("controllerLog()") //在切入点的方法run之前要干的
    public void beforMethod(JoinPoint joinPoint){
		requestAttributes = RequestContextHolder.currentRequestAttributes();//这个RequestContextHolder是Springmvc提供来获得请求的东西
		currenttime.set(System.currentTimeMillis());
		HttpServletRequest request=((ServletRequestAttributes)requestAttributes).getRequest();
		AdminUser adminUser= null;
		if (request != null) {
			HttpSession session = request.getSession();
			adminUser = (AdminUser) session.getAttribute("user");
		}
		if (adminUser != null) {
			userId.set(adminUser.getId());
		}
    }

    /**
     * 返回通知(在方法正常结束执行的代码)
     * 返回通知可以访问到方法的返回值!
     * @param joinPoint
     */
    @AfterReturning(returning = "result",pointcut = "controllerLog()")
    public void afterReturnMethod(JoinPoint joinPoint,Object result){
        long aftertime=System.currentTimeMillis();
        String ss=aftertime-currenttime.get()+"";
        String methodName = joinPoint.getSignature().getName();
        String resultString=JSONObject.toJSONString(result);
        HttpServletRequest request=((ServletRequestAttributes)requestAttributes).getRequest();
        if(request==null){
        	return;
        }
        String urlname=request.getServletPath();
        String  host=request.getLocalName();
        LogOperVO param=new LogOperVO();
        param.setResult(resultString);
        Enumeration<String> enumer=request.getParameterNames();
		Map<String ,String> map=new HashMap<String,String>();
		while(enumer.hasMoreElements()){				
			String key=(String)enumer.nextElement();
			String value=request.getParameter(key);
			map.put(key, value);
		}
		String ip = getRemoteIP(request);
		String params=JSONObject.toJSONString(map);
        param.setHostname(host);
		param.setIp(ip);
		param.setMethodname(methodName);
		param.setParams(params);
		param.setUrlname(urlname);
		param.setResult(resultString);
		param.setRuntime(ss);
		if(userId!=null){
			param.setUserid(userId.get());
		}	
		opDatabaseService.insLogOperVO(param);//这行是用来添加日志的,自己定义注入即可
    }
	@AfterThrowing(pointcut = "controllerLog()", throwing = "ex")
	public void doAfterThrowing(JoinPoint joinPoint, Exception ex) {
		String methodName = joinPoint.getSignature().getName();
		List<Object> args = Arrays.asList(joinPoint.getArgs());
		System.out.println("连接点方法为:" + methodName + ",参数为:" + args + ",异常为:" + ex);
		LogErrorlog pojo=new LogErrorlog();
		pojo.setName(joinPoint.getSignature().getName());
		pojo.setClassName(joinPoint.getSignature().getDeclaringTypeName());
		pojo.setErrorMessage(ex.getMessage());
		pojo.setErrorType(ex.toString());
		String erro=errorToString(ex);
		pojo.setErrorText(erro);
		Date date =new Date();
		SimpleDateFormat format =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String fdate=format.format(date);
		pojo.setCreatDate(fdate);
		opDatabaseService.insLogErrorlog(pojo);
	}
	//将异常信息转化成字符串
	private static String errorToString(Throwable e) {
		StringWriter sw = new StringWriter();
		PrintWriter pw = new PrintWriter(sw, true);
		e.printStackTrace(pw);
		pw.flush();
		sw.flush();
		pw.close();
		return sw.toString();
	}
	//获取id
	private String getRemoteIP(HttpServletRequest request) {
		String ip =null;
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("X-Forwarded-For");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("WL-Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getRemoteAddr();
		}
		if (ip != null) {
			//对于通过多个代理的情况,最后IP为客户端真实IP,多个IP按照','分割
			int position = ip.indexOf(",");
			if (position > 0) {
				ip = ip.substring(0, position);
			}
		}
		return ip;
	}
}

3.注解说明

@Aspect和@Component
首先,这个@Aspect注释告诉Spring这是个切面类,然后@Compoment将转换成Spring容器中的bean或者是代理bean。 总之要写切面这两个注解一起用就是了。

既然是切面类,那么肯定是包含PointCut还有Advice两个要素的,下面对几个注解展开讲来看看在@Aspect中是怎么确定切入点(PointCut)和增强通知(Advice)的。

@PointCut

这个注解包含两部分,PointCut表达式和PointCut签名。表达式是拿来确定切入点的位置的,说白了就是通过一些规则来确定,哪些方法是要增强的,也就是要拦截哪些方法。

@PointCut(...........)括号里面那些就是表达式。这里的execution是其中的一种匹配方式,还有:

execution: 匹配连接点

within: 某个类里面

this: 指定AOP代理类的类型

target:指定目标对象的类型

args: 指定参数的类型

bean:指定特定的bean名称,可以使用通配符(Spring自带的)

@target: 带有指定注解的类型

@args: 指定运行时传的参数带有指定的注解

@within: 匹配使用指定注解的类

@annotation:指定方法所应用的注解

 注意,由于是动态代理的实现方法,所以不是所有的方法都能拦截得下来,对于JDK代理只有public的方法才能拦截得下来,对于CGLIB只有public和protected的方法才能拦截。

这里我们主要介绍execution的匹配方法,因为大多数时候都会用这个来定义pointcut:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
            throws-pattern?)

execution(方法修饰符(可选)  返回类型  类路径 方法名  参数  异常模式(可选)) 

除了返回类型,方法名还有参数之外,其他都是可选的

 ret-type-pattern:可以为*表示任何返回值,全路径的类名等.

name-pattern:指定方法名,*代表所以,set*,代表以set开头的所有方法.
parameters pattern:指定方法参数(声明的类型),         ()匹配没有参数;  (..)代表任意多个参数;   (*)代表一个参数,但可以是任意型;    (*,String)代表第一个参数为任何值,第二个为String类型。

下面给几个例子:

1)execution(public * *(..))——表示匹配所有public方法

2)execution(* set*(..))——表示所有以“set”开头的方法

3)execution(* com.xyz.service.AccountService.*(..))——表示匹配所有AccountService接口的方法

4)execution(* com.xyz.service.*.*(..))——表示匹配service包下所有的方法

5)execution(* com.xyz.service..*.*(..))——表示匹配service包和它的子包下的方法

@Before

这个是决定advice在切入点方法的什么地方执行的标签,这个注解的意思是在切入点方法执行之前执行我们定义的advice。

@Before("controllerLog()") //在切入点的方法run之前要干的
    public void logBeforeController(JoinPoint joinPoint) {
    
    

@After

这个注解就是在切入的方法运行完之后把我们的advice增强加进去。一样方法中可以添加JoinPoint。

@Around

这个注解可以简单地看作@Before和@After的结合。这个注解和其他的比比较特别,它的方法的参数一定要是ProceedingJoinPoint,这个对象是JoinPoint的子类。我们可以把这个看作是切入点的那个方法的替身,这个proceedingJoinPoint有个proceed()方法,相当于就是那切入点的那个方法执行,简单地说就是让目标方法执行,然后这个方法会返回一个对象,这个对象就是那个切入点所在位置的方法所返回的对象。

除了这个Proceed方法(很重要的方法),其他和那几个注解一样。

@AfterReturning

顾名思义,这个注解是在目标方法正常完成后把增强处理织入。这个注解可以指定两个属性(之前的三个注解后面的括号只写一个@PointCut表达式,也就是只有一个属性),一个是和其他注解一样的PointCut表达式,也就是描述该advice在哪个接入点被织入;然后还可以有个returning属性,表明可以在Advice的方法中有目标方法返回值的形参。

@AfterReturning(returning = "returnOb", pointcut = "controllerLog()")
    public void doAfterReturning(JoinPoint joinPoint, Object returnOb) {
        System.out.println("the return of the method is : " + returnOb);
    }

@AfterThrowing

异常抛出增强,在异常抛出后织入的增强。有点像上面的@AfterReturning,这个注解也是有两个属性,pointcut和throwing。

用法也和刚刚的那个returning差不多:

@AfterThrowing(pointcut = "controllerLog()", throwing = "ex")
public void doAfterThrowing(JoinPoint joinPoint, Exception ex) {
        String methodName = point.getSignature().getName();
        List<Object> args = Arrays.asList(point.getArgs());
        System.out.println("连接点方法为:" + methodName + ",参数为:" + args + ",异常为:" + ex);
          
}   

这里还要提到上面用到的一个类:RequestContextHolder

比如说,有个需求需要在service中获得request和response,我们一般会(我就是)直接在controller那把request或response作为参数传到service,这就很不美观。后来知道,原来SpringMVC提供了个很强大的类ReqeustContextHolder,通过他你就可以获得request和response什么的。

//下面两个方法在没有使用JSF的项目中是没有区别的
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
//                                            RequestContextHolder.getRequestAttributes();
//从session里面获取对应的值
String str = (String) requestAttributes.getAttribute("name",RequestAttributes.SCOPE_SESSION);
 
HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
HttpServletResponse response = ((ServletRequestAttributes)requestAttributes).getResponse();

最后,再记录一下各个不同的advice的拦截顺序的问题。

情况一,只有一个Aspect类:

  无异常:@Around(proceed()之前的部分) → @Before → 方法执行 → @Around(proceed()之后的部分) → @After → @AfterReturning

  有异常:@Around(proceed(之前的部分)) → @Before → 扔异常ing → @After → @AfterThrowing    (大概是因为方法没有跑完抛了异常,没有正确返回所有@Around的proceed()之后的部分和@AfterReturning两个注解的加强没有能够织入)

情况二,同一个方法有多个@Aspect类拦截:

  单个Aspect肯定是和只有一个Aspect的时候的情况是一样的,但不同的Aspect里面的advice的顺序呢??答案是不一定,像是线程一样,没有谁先谁后,除非你给他们分配优先级,同样地,在这里你也可以为@Aspect分配优先级,这样就可以决定谁先谁后了。

优先级有两种方式:

  • 实现org.springframework.core.Ordered接口,实现它的getOrder()方法
  • 给aspect添加@Order注解,该注解全称为:org.springframework.core.annotation.Order

不管是哪种,都是order的值越小越先执行:

@Order(5)
@Component
@Aspect
public class Aspect1 {
    // ...
}

@Order(6)
@Component
@Aspect
public class Aspect2 {
    // ...
}

这样Aspect1就永远比Aspect2先执行了。

注意点:

  • 如果在同一个 aspect 类中,针对同一个 pointcut,定义了两个相同的 advice(比如,定义了两个 @Before),那么这两个 advice 的执行顺序是无法确定的,哪怕你给这两个 advice 添加了 @Order 这个注解,也不行。这点切记。
  • 对于@Around这个advice,不管它有没有返回值,但是必须要方法内部,调用一下 pjp.proceed();否则,Controller 中的接口将没有机会被执行,从而也导致了 @Before这个advice不会被触发。

猜你喜欢

转载自blog.csdn.net/z19799100/article/details/109677470