spring boot 五中类型通知及统一日志处理

引言

Spring AOP是一个对AOP原理的一种实现方式,另外还有其他的AOP实现如AspectJ等。

AOP意为面向切面编程,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术,是OOP面向对象编程的一种补足。它是软件开发中的一个热点技术,Spring AOP 也是Spring框架的核心特性之一(另一个核心特性是IOC)。

通过AOP技术,我们希望实现一种通用逻辑的解耦,解决一些系统层面上的问题,如日志、事务、权限等,从而提高应用的可重用性和可维护性,和开发效率。

Struts2的拦截器设计就是基于AOP的思想,是非常经典的理论实践案例。

重要概念

AOP中包括 5 大核心概念:切面(Aspect)、连接点(JoinPoint)、通知(Advice)、切入点(Pointcut)、AOP代理(Proxy)。

关于前面四点,将会直接涉及到相关编码的实现方式,因此将会结合代码进行解释,在这里简单阐述一下AOP代理。

AOP代理,是AOP框架如Spring AOP创建的对象,代理就是对目标对象进行增强,Spring AOP中的代理默认使用JDK动态代理,同时支持CGLIB代理,前者基于接口,后者基于子类。在Spring AOP中,其功能依然离不开IOC容器,代理的生成、管理以及其依赖关系都是由IOC容器负责,而根据目前的开发提倡“面向接口编程”,因此大多使用JDK动态代理。

五大通知类型

1、前置通知 [ Before advice ] :在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常;

2、正常返回通知 [ After returning advice ] :在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行;

3、异常返回通知 [ After throwing advice ] :在连接点抛出异常后执行;

4、返回通知 [ After (finally) advice ] :在连接点执行完成后执行,不管正常执行完成,还是抛出异常,都会执行返回通知中的内容;

5、环绕通知 [ Around advice ] :环绕通知围绕在连接点前后,比如一个方法调用的前后。这种通知是最强大的通知,能在方法调用前后自定义一些操作。

应用案例分析

在OOP中的基本单元是类,而在AOP中的基本单元是Aspect,它实际上也是一个类,只不过这个类用于管理一些具体的通知方法和切入点。

所谓的连接点,实际上就是一个具体的业务方法,比如Controller中的一个请求方法,而切入点则是带有通知的连接点,在程序中主要体现为书写切入点表达式,这个表达式将会定义一个连接点。

就以Controller中的一个请求方法为例,通过AOP的方式实现一定的业务逻辑。

这个逻辑是:GET请求某一方法,然后通过一个Aspect来实现在这个方法调用前和调用后做一些日志输出处理。

引入依赖jar包

基于spring boot 的maven依赖如下,如果是仅使用spring框架的话,请参考其他资料:

<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>

定义业务方法(连接点)

这个方法就是后面AOP切面中的那个连接点,方法非常简单,仅仅接收一个姓名和性别,并输出 “某某做作业......” :

@RestController
publicclassDoHomeWorkController{
@GetMapping("/dohomework")
publicvoiddoHomeWork(String name, Gender gender){
System.out.println(name + "做作业... ...");
}
}

定义切面类、定义切入点及通知方法

下面的代码中,@Aspect、@Pointcut、@Component都是必须的(@Component用于将这个切面类注入到 IOC容器中,如果不用@Component就用@Bean的方式也是可以的,但总之切面类必须被注入到 IOC容器中,这也就是前面说的Spring AOP不能脱离IOC容器的体现)。而@Before用来定义一个前面提到过的五大通知类型中的 Before advice类型的通知方法,这个根据具体的需要可以进行选择。

@Pointcut注解的参数是一个表达式,可以当做是一个固定的写法,“ * ” 表示任意返回值,“ .. ” 也是一种通配。当然,方法的全名可以使用编辑器的复制功能,具体关于execution表达式的说明,在此不做展开讨论。

@Aspect
@Component
publicclassDoHomeWorkAspect{
/** 定义切入点 */
@Pointcut("execution(* com.example.demo.controller.DoHomeWorkController.doHomeWork(..))")
publicvoidhomeWorkPointcut(){
}
/** 定义Before advice通知类型处理方法 */
@Before("homeWorkPointcut()")
publicvoidbeforeHomeWork(){
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
System.out.println(request.getParameter("name") + "想先吃个冰淇淋......");
}
}

再简单说一下通过RequestContextHolder这个最终获取request的操作,就当是一个固定写法,可以从请求上下文中拿到当前的请求对象,并从请求中获得一些信息,更详细的API用法不做展开。

执行结果

控制台显示如下:

可以从输出结果中看到,在执行doHomeWork(String name, Gender gender) 方法之前先执行了切面类中定义的beforeHomeWork()方法,成功的完成了在切入点之前执行一个操作的需要。这就是Spring AOP的典型应用。

环绕通知实现

在上一节“应用案例分析”中介绍了Before advice的使用方式,而Spring AOP的通知类型有五种,在Spring 框架里分别有对应的注解来代表每一种通知类型,它们分别是:

@Before 对应——>前置通知 [ Before advice ]

@AfterReturning 对应——>正常返回通知 [ After returning advice ]

@AfterThrowing 对应——>异常返回通知 [ After throwing advice ]

@After 对应——>返回通知 [ After (finally) advice ]

@Around 对应——>环绕通知 [ Around advice ]

其中,前四种通知类型,与@Before的使用完全相同,根据各自不同的使用定义自行选择。

需要说明的是@Around的使用。在定义环绕通知方法的时候,需要传入一个org.aspectj.lang.ProceedingJoinPoint 对象:

@Around("homeWorkPointcut()")
publicvoidaround(ProceedingJoinPoint joinPoint){
System.out.println("环绕通知,方法执行前");
try {
joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("环绕通知,方法执行后");
}

执行结果如下:

根据输出结果,我们注意到了一个问题,即@Around先于@Before通知执行。这就引出了一个非常重要的问题,即各类型通知执行的先后顺序。

各类型通知执行先后顺序

在实际开发中,有时候我们会针对同一个切入点进行多种Aspect包装,比如,可以有一个Aspect管理对一个方法进行日志打印的通知,而另一个Aspect管理对这个方法的一些校验工作。因此,涉及到两类问题:

1、同一个切入点不同通知的执行顺序

2、同一个切入点不同切面的执行顺序

我们在前面的“环绕通知实现”结果中看到,@Around是先于@Before执行的,这就是其中一个问题的引出,即同一个切入点不同通知的执行顺序。来看下面这张图:

 可以看到Aspect1 和Aspect2两个切面类中所有通知类型的执行顺序,Method是具体的切入点,order代表优先级,它根据一个int值来判断优先级的高低,数字越小,优先级越高!所以,不同的切面,实际上是环绕于切入点的同心圆:

 @Order注解改变优先级

@order注解可以使用在类或方法上,但是,直接作用于方法上是无法奏效的,目前的使用方法都是通过标记在切面类上,来实现两个切面的优先级。

@Order注解接收一个int类型的参数,这个参数可以是任意整型数值,数值小的,优先级高。

对于使用@Order来改变通知方法执行的优先级,亲测无法生效。也就是说就算你使用@Order注解,让@Before的优先级高于@Around也依然不会得到想要的结果,而且,如果在一个Aspect类中有两个@Before,并使用@Order来分配这两个@Before的优先级依然不会生效。

因此,在实际开发的过程中,应该避免在一个Aspect类中有多个相同的通知类型,否则,就算使用@Order来区分优先级,可能最后的效果也不符预期。

那么,关于@Order注解实现优先级的方式,我个人总结了以下几条经验:

1、在一个Aspect类中不要有多个同种类型的通知,如多个@Before、多个@After;

2、不要在通知方法上使用@Order来区分优先级,要遵循默认的通知方法优先级(同心圆模型);

3、如果避免不了有相同类型的通知,要区分在不同的Aspect类中,并且通过@Order(1)、@Order(2)、@Order(3)... 来区分Aspect类的优先级,即以切面类作为优先级的区分单元,而不是通知方法;

4、在编写多个通知方法时,应当把实际业务需要与默认通知优先级(同心圆模型)结合编码。

通知节点传参

首先带参数的切点声明:

@Pointcut(value="execution(* com.test.enterprise.service..*.*(..)) && args(param)",argNames = "param")
    public void recordOrderLog(Map param){
    }

在pointcut中制定切面,及指定切面参数名称param

Before参数设置

@Before(value = "recordOrderLog(param)")
public void doBefore(JoinPoint joinPoint, Map param)

afterreturning参数设置

@AfterReturning(value = "recordOrderLog(param)",returning="returnMap")
 public void doAfterReturn(JoinPoint joinPoint, Map param,Object returnMap){

参数为连接点,入参,出参   ,returning 指定那个参数是出参

AfterThrowing异常参数设置

@AfterThrowing(value = "recordOrderLog(param)",throwing = "exception")
    public Object doAfterThrowing(JoinPoint joinPoint,Map param,Throwable exception){

参数为连接点,入参,出参   ,指定异常参数

Around 参数设置

@Around(value = "recordOrderLog(param)")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint, Map param) throws Throwable{

第一个参数必须是:proceedingJoinPoint 包含链接点信息

统一日志的实现

需要考虑的问题:

      1: 如果在程序异常系统回滚时,保证日志不回滚?

       2:程序正常执行日志,程序正常返回日志,程序正常返回错误日志,程序执行异常日志怎么记录?

       3: 异常报错怎么反馈到调用方?

给予这两个问题,考虑的实现代码如下:

/**
     * 如果正常返回,则记录正常日志
     * @param param
     */
    @Around(value = "recordOrderLog(param)")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint, Map param) throws Throwable{
        Object result = null;
        String inParamString = JSONUtils.mapToJson(param).toJSONString();
        String declaringTypeName = proceedingJoinPoint.getSignature().getDeclaringTypeName();
        String methodName = proceedingJoinPoint.getSignature().getName();
        MonitorOrderMsg orderMsg = new MonitorOrderMsg();
        orderMsg.setErrorFlag("0");
        orderMsg.setMethod(declaringTypeName+"."+methodName );
        orderMsg.setInTime(new Date());
        orderMsg.setRequstMsg(inParamString.length()>3999?inParamString.substring(0,3999):inParamString);
        String errorInfo = "";
        try{
            long statrTime=System.currentTimeMillis();
            result = proceedingJoinPoint.proceed();
            long endTime=System.currentTimeMillis();
            orderMsg.setCostTime(Long.toString(endTime-statrTime));
            StringBuilder spendTimeLog=new StringBuilder("方法名称:").append(declaringTypeName).append(".").append(methodName).append("() 耗时为:").append(endTime-statrTime).append("ms.");
            spendTimeLog.append("输入参数为:"+ inParamString);
            String outString = JSONUtils.mapToJson(((Map )result)).toJSONString();
            spendTimeLog.append("输出参数为:"+outString);
            logger.info(spendTimeLog.toString());
            orderMsg.setResponseMsg(outString.length()>3999?outString.substring(0,3999):outString);
            if("1".equals(((Map)result).get(IConstant.RESP_CODE))){
                orderMsg.setErrorFlag("1");
                orderMsg.setErrorMsg(((Map)result).get(IConstant.RESP_CODE).toString());
                /*if(!param.containsKey("errorDeal")){
                    monitorOrderMsgRepository.save(orderMsg);
                }*/

            }
        }catch (Throwable throwable) {
            errorInfo = throwable.getLocalizedMessage();
        }finally {
            if(StringUtil.isNotBlank(errorInfo)){
                CommonRep com = new CommonRep("1",errorInfo);
                return com.getResult();
            }else{
                return result;
            }
        }
    }


/**
     * 02  .后置返回通知
     * 需要注意:
     *      如果第一个参数是JoinPoint,则第二个参数是返回值的信息
     *      如果参数中的第一个不是JoinPoint,则第一个参数是returning中对应的参数,
     *    returning 限定了只有目标方法返回值与通知方法相应参数类型时才能
     * 执行后置返回通知,否则不执行;
     * 对于returning对应的通知方法参数为Object类型将匹配任何目标返回值
     * @param joinPoint
     * @param returnMap
     */
    //记录日志问题,不适合使用环绕通知
    // 另起事物,否则会异常回滚
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @AfterReturning(value = "recordOrderLog(param)",returning="returnMap")
    public void doAfterReturn(JoinPoint joinPoint, Map param,Object returnMap){
        Signature signature=joinPoint.getSignature(); //  通知的签名
        String declaringTypeName =signature.getDeclaringTypeName();   //类名称
        String methodName = signature.getName(); // 方法名称
        String outParamString = JSONUtils.mapToJson((Map<String, Object>) returnMap).toJSONString();
        String inParamString = JSONUtils.mapToJson(param).toJSONString();
        StringBuilder spendTimeLog=new StringBuilder("方法名称:").append(declaringTypeName).append(".").append(methodName).append("() 输出参数为:"+outParamString);
        logger.info(spendTimeLog.toString());

        // 先屏蔽掉正常返回的日志记录
       MonitorOrderMsg orderMsg = new MonitorOrderMsg();
        orderMsg.setErrorFlag("0");
        orderMsg.setMethod(declaringTypeName+"."+methodName );
        orderMsg.setInTime(new Date());
        orderMsg.setRequstMsg(inParamString.length()>3999?inParamString.substring(0,3999):inParamString);
        orderMsg.setResponseMsg(outParamString.length()>3999?outParamString.substring(0,3999):outParamString);
        if("1".equals(((Map)returnMap).get(IConstant.RESP_CODE))){
            orderMsg.setErrorFlag("1");
            orderMsg.setErrorMsg(((Map)returnMap).get(IConstant.RESP_CODE).toString());
            try {
                if(!param.containsKey("errorDeal")){
                    errorMsgForTask.setErrorMsg(errorMsgForTask.getKeyTime(declaringTypeName+"."+methodName),inParamString);
                    monitorOrderMsgRepository.save(orderMsg);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }

实现说明:

     @Around      处理说明

1: 使用环绕通知来捕捉异常信息,转换异常报错为正常报错   这么处理的原因是为了返回报错信息到调用方,特别是web应用的前台,防止页面假死无反应!

2:  程序正常的报错信息不处理,交给AfterReturning 处理

@AfterReturning 处理说明

  1:  根据返回判断是否异常,异常则记录日志

  2:   开启新事物,防止在回滚数据时,日志被回滚掉!

在日志处理是走过的坑

 

  1: 不建议使用AfterThrowing 做日志记录,存在的问题是: 当AfterThrowing 捕捉到异常时,则必定无数据返回到前台,随说可以记录到日志,但是前台反馈不友好

 2:  不能再通知中抛出异常,这种异常不会被异常通知捕捉

猜你喜欢

转载自blog.csdn.net/wangan_0601/article/details/88399558