一、aop日志简述

AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善,是spring中最核心的原则。在实际开发中,日志打印是非常重要的,一般都需要对请求的出参入参以及处理时间进行打印。为了提高代码的重用性,一般将这些日志打印放在aop中。

二、代码示例

1.引入jar包依赖

<!-- 对aop的支持 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.aop编写

package com.wangcongming.aop;

import com.alibaba.fastjson.JSON;
import com.wangcongming.annotation.LogDescription;
import com.wangcongming.annotation.LogType;
import com.wangcongming.util.CollectionUtils;
import com.wangcongming.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Map;

/**
 * @author wangcongming
 * @version V1.0
 * @ClassName LogAspect
 * @Description 日志切面
 *              @Pointcut 定义切点 有两种写法
 *              1. @Pointcut("execution(public * com.learn.blog.controller.*.*(..))")
 *              2. @Pointcut("@annotation(com.chhliu.springboot.aop.annotation.RedisCache)") 注解类型
 *              @Before  定义前置通知
 *              获取请求信息
 *              ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
 *              HttpServletRequest request = attributes.getRequest();
 * @date 2018/4/7 18:11
 */
@Aspect
@Component
@Slf4j
public class LogAspect {

    /**
     * 定义切点
     */
    @Pointcut("@within(com.wangcongming.annotation.AopLog)")
    public void logPointcut(){}

    /**
     *
     * @param joinPoint
     * @throws Throwable
     */
    @Before("logPointcut()")
    public void deBefore(JoinPoint joinPoint) throws Throwable {

    }
  
    @AfterReturning(returning = "ret", pointcut = "logPointcut()")
    public void doAfterReturning(Object ret) throws Throwable {
    }
  
    //后置异常通知  
    @AfterThrowing(pointcut = "logPointcut()",throwing = "e")
    public void error(JoinPoint jp, Throwable e){
        log.error(getUrl() + ":请求异常<<<<<<<<<<<<<<<<<<<<<<<<<",e);
    }  
  
    //后置最终通知,final增强,不管是抛出异常或者正常退出都会执行  
    @After("logPointcut()")
    public void after(JoinPoint jp){
    }

    /**
     * 用于打印日志
     * 当未使用注解时
     * @param pjp
     * @return
     */
    //环绕通知,环绕增强,相当于MethodInterceptor  
    @Around("logPointcut()")
    public Object around(ProceedingJoinPoint pjp) {
        long currentTime = System.currentTimeMillis();
        try {
            //拦截的实体类
            Object target = pjp.getTarget();
            //拦截方法的参数
            Object[] args = pjp.getArgs();
            //拦截的方法名称  
            String methodName = pjp.getSignature().getName();
            LogDescription logDes = this.getLogDes(pjp);
            String name = "";
            if(logDes == null){
                name = getUrl();
                return dealLog(name,methodName,args,currentTime,pjp);
            }else{
                name = logDes.name();
                name = StringUtils.isEmpty(name) ? getUrl() : name;
                LogType[] type = logDes.type();
                Map<Object, Object> map = CollectionUtils.convertMap(type);
                if(map.get(LogType.ALL) != null){
                    return dealLog(name, methodName,args, currentTime,pjp);
                }
                if(map.get(LogType.PARAMETER) != null){
                    log.info("|--->{}<--|<<{}>>请求参数:{}",methodName,name,args);
                }
                //执行方法,并获取返回结果
                Object o =  pjp.proceed();
                if(map.get(LogType.RESULT) != null){
                    log.info("|--->{}<--|<<{}>>请求返回结果:{}",methodName,name, JSON.toJSONString(o));
                }
                if(map.get(LogType.TIME) != null){
                    long endTime = System.currentTimeMillis();
                    log.info("|--->{}<--|<<{}>>请求耗时:{}ms******************",methodName,name,(endTime - currentTime));
                }
                return o;
            }
        } catch (Throwable e) {
            log.error("aop异常",e);
            return null;  
        }  
    }

    private Object dealLog(String name,String methodName,Object[] args,long currentTime,ProceedingJoinPoint pjp){
        log.info("|--->{}<--|<<{}>>请求参数:{}",methodName,name,args);
        try {
            Object o = pjp.proceed();
            log.info("|--->{}<--|<<{}>>请求返回结果:{}",methodName,name, JSON.toJSONString(o));
            long endTime = System.currentTimeMillis();
            log.info("|--->{}<--|<<{}>>请求耗时:{}ms******************",methodName,name,(endTime - currentTime));
            return o;
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return null;
    }

    private LogDescription getLogDes(ProceedingJoinPoint pjp){
        Method method = getMethod(pjp);
        if(method == null){
            log.error("无法获取到目标方法对象。。。。。");
            return null;
        }
        LogDescription annotation = method.getAnnotation(LogDescription.class);
        return annotation;
    }

    /**
     * 获取打印日志的标识符,优先使用注解LogDescription 中的name,获取不到则使用url
     * @return
     */
    private String getName(ProceedingJoinPoint pjp){
        LogDescription logDes = this.getLogDes(pjp);
        //注解不存在
        if(logDes == null){
            return getUrl();
        }
        String name = logDes.name();
        //注解中name未指明值
        if(StringUtils.isEmpty(name)){
            return getUrl();
        }
        return name;
    }

    /**
     * 获取所拦截的目标方法 Method
     * @param pjp
     * @return
     */
    private Method getMethod(ProceedingJoinPoint pjp){
        Object[] args = pjp.getArgs();
        Class<?>[] argTypes = new Class[pjp.getArgs().length];
        for (int i = 0; i < args.length; i++) {
            argTypes[i] = args[i].getClass();
        }
        Method method = null;
        try {
            method = pjp.getTarget().getClass().getMethod(pjp.getSignature().getName(), argTypes);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        }
        return method;
    }

    /**
     * 获取当前请求的url
     * @return
     */
    private String getUrl(){
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String url = request.getRequestURI().toString();
        return url;
    }

    /**
     * 获取请求参数列表
     * @param request
     * @return
     */
    private String getParams(HttpServletRequest request){
        Enumeration<String> parameterNames = request.getParameterNames();
        StringBuffer params = new StringBuffer();
        while (parameterNames.hasMoreElements()){
            String name = parameterNames.nextElement();
            String value = request.getParameter(name);
            params.append(String.format("参数:[%s=%s] ",name,value));
        }
        return params.toString();
    }
}

代码中的logPointcut()是对注解进行切入的,也可以对包进行切入。使用注解切入,有一个好处就是,logaop可以单独封装成一个jar包,直接引入就可使用。

其中@AopLog @LogDescription都是自定义的注解,代码如下:

package com.wangcongming.annotation;

import java.lang.annotation.*;

/**
 * @author wangcongming
 * @version V1.0
 * @ClassName AopLog
 * @Description 自定义log注解 该注解只能被使用在controller层
 * @date 2018/4/7 18:11
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface AopLog {

    String value() default "";
}
package com.wangcongming.annotation;

import java.lang.annotation.*;

/**
 * @author wangcongming
 * @version V1.0
 * @ClassName LogDescription
 * @Description 自定义log注解 该注解只能被使用在controller层
 * @date 2018/4/7 18:11
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogDescription {

    //标识符,建议使用url
    String name() default "";
    //打印日志类型
    LogType[] type() default {};
}

三、pointcut的其他用法

Pointcut 是指那些方法需要被执行"AOP",是由"Pointcut Expression"来描述的.
Pointcut可以有下列方式来定义或者通过&& || 和!的方式进行组合. 
args()
@args()
execution()
this()
target()
@target()
within()
@within()
@annotation
其中execution 是用的最多的,其格式为:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
returning type pattern,name pattern, and parameters pattern是必须的.
ret-type-pattern:可以为*表示任何返回值,全路径的类名等.
name-pattern:指定方法名,*代表所以,set*,代表以set开头的所有方法.
parameters pattern:指定方法参数(声明的类型),(..)代表所有参数,(*)代表一个参数,(*,String)代表第一个参数为任何值,第二个为String类型.
举例说明:
任意公共方法的执行:
execution(public * *(..))
任何一个以“set”开始的方法的执行:
execution(* set*(..))
AccountService 接口的任意方法的执行:
execution(* com.xyz.service.AccountService.*(..))
定义在service包里的任意方法的执行:
execution(* com.xyz.service.*.*(..))
定义在service包和所有子包里的任意类的任意方法的执行:
execution(* com.xyz.service..*.*(..))
定义在pointcutexp包和所有子包里的JoinPointObjP2类的任意方法的执行:
execution(* com.test.spring.aop.pointcutexp..JoinPointObjP2.*(..))")

***> 最靠近(..)的为方法名,靠近.*(..))的为类名或者接口名,如上例的JoinPointObjP2.*(..))

pointcutexp包里的任意类.
within(com.test.spring.aop.pointcutexp.*)
pointcutexp包和所有子包里的任意类.
within(com.test.spring.aop.pointcutexp..*)
实现了Intf接口的所有类,如果Intf不是接口,限定Intf单个类.
this(com.test.spring.aop.pointcutexp.Intf)
***> 当一个实现了接口的类被AOP的时候,用getBean方法必须cast为接口类型,不能为该类的类型.

带有@Transactional标注的所有类的任意方法.
@within(org.springframework.transaction.annotation.Transactional)
@target(org.springframework.transaction.annotation.Transactional)
带有@Transactional标注的任意方法.
@annotation(org.springframework.transaction.annotation.Transactional)
***> @within和@target针对类的注解,@annotation是针对方法的注解

参数带有@Transactional标注的方法.
@args(org.springframework.transaction.annotation.Transactional)
参数为String类型(运行是决定)的方法.
args(String)