SpringBoot 自定义注解 + AOP 实现必填参数非空校验、接口传入参数和应答数据打印、方法耗时统计

Java Web 项目,controller 层经常需要校验传入参数必填且非空、接口传入参数打印、接口应答数据打印和方法耗时统计等功能。为了简化开发,可以通过自定义注解方式,将各个接口相通的功能点抽离到拦截器,统一实现。本文以 SpringBoot 为例,将实现方式陈述如下。

一、自定义注解
自定义注解 Check,注解参数为 String 型数组,数组中各元素为必填参数属性名。

package com.example.asynctask.globconf;

import java.lang.annotation.*;

/**
 * @description 自定义拦截器标签,params为必填属性
 * @date 2019/3/20 9:31
 */
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Check {
    String[] params();
}

二、定义全局异常类,若参数缺失则抛出异常

package com.example.asynctask.entity;

public class CommonException extends RuntimeException {

    private static final long serialVersionUID = -238091758285157331L;

    private String errCode;

    private String errMsg;

    public CommonException() {
        super();
    }

    public CommonException(String message, Throwable cause) {
        super(message, cause);
    }

    public CommonException(String message) {
        super(message);
    }

    public CommonException(Throwable cause) {
        super(cause);
    }

    public CommonException(String errCode, String errMsg) {
        super(errCode + ":" + errMsg);
        this.errCode = errCode;
        this.errMsg = errMsg;
    }

    public String getErrCode() {
        return this.errCode;
    }

    public String getErrMsg() {
        return this.errMsg;
    }
}

三、AOP 拦截器方式拦截注解标签,实现公共方法

package com.example.asynctask.globconf;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.example.asynctask.entity.CommonException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * @description 拦截器,实现传入参数打印、必填参数非空校验、传出参数打印、接口耗时统计等功能。
 * @date 2019/3/25 18:47
 */
@Aspect
@Component
public class ParamsAspect {
    private static final Logger logger = LoggerFactory.getLogger(ParamsAspect.class);

    private static String dateFormat = "yyyy-MM-dd HH:mm:ss";

    @Pointcut("@annotation(com.example.asynctask.globconf.Check)")
    public void paramsCheck() {
    }

    @Around("paramsCheck()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1、记录方法开始执行的时间
        long start = System.currentTimeMillis();

        // 2、打印请求参数
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String target = joinPoint.getSignature().getDeclaringTypeName();              // 全路径类名
        String classNm = target.substring(target.lastIndexOf(".") + 1, target.length()); // 类名截取
        String method = joinPoint.getSignature().getName();                          // 获取方法名
        Map<String, String> params = getAllRequestParam(request);                    // 获取请求参数
        logger.info("{}.{} 接收参数: {}", classNm, method, JSON.toJSONString(params));

        Check check = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(Check.class); // 获取注解
        String[] requiredFields = check.params();                                   // 获取注解参数
        // 3、必填参数非空校验
        Boolean result = validParams(params, requiredFields);
        if (result) {

            Object object = joinPoint.proceed();        // 必填参数非空校验通过,执行方法,获取执行结果
            // 4、打印应答数据和方法耗时
            long time = System.currentTimeMillis() - start;
            logger.info("{}.{} 应答数据: {}; 耗时 {} ms", classNm, method, JSONObject.toJSONStringWithDateFormat(object,
                    dateFormat, SerializerFeature.WriteMapNullValue), time);
            return object;
        } else {
            // 必填参数非空校验未通过,抛出异常,由GlobalExceptionHandler捕获全局异常,返回调用方“参数缺失”
            throw new CommonException("2", "参数缺失或格式错误");
        }
    }

    /**
     * 校验传入参数params(非null)中是否必含requiredFields(非null)中的各个属性,且属性值非空
     *
     * @param params         传入参数
     * @param requiredFields 设置的非空属性数组
     * @return 校验通过返回true,否则返回false
     */
    private Boolean validParams(Map<String, String> params, String[] requiredFields) {
        if (requiredFields.length == 0) {
            // 无必送参数,直接返回true
            return true;
        } else {
            for (String field : requiredFields) {
                if (StringUtils.isEmpty(params.get(field))) {
                    return false;
                }
            }
            return true;
        }
    }

    /**
     * 获取请求参数
     */
    public static Map<String, String> getAllRequestParam(HttpServletRequest request) {
        Map<String, String> res = new HashMap<>();
        Enumeration<?> temp = request.getParameterNames();
        if (null != temp) {
            while (temp.hasMoreElements()) {
                String en = (String) temp.nextElement();
                String value = request.getParameter(en);
                res.put(en, value);
                // 在报文上送时,如果字段的值为空,则不上送<下面的处理为在获取所有参数数据时,判断若值为空,则删除这个字段>
                if (StringUtils.isEmpty(res.get(en))) {
                    res.remove(en);
                }
            }
        }
        return res;
    }

    /**
     * 前置通知(任何时候进入连接点都调用)
     *
     * @param joinPoint joinPoint
     */
//    @Before("paramsCheck()")
//    public void before(JoinPoint joinPoint) {
//        System.out.println("do before。");
//    }

    /**
     * 后通知,当某连接点退出时执行(无论连接点正常退出还是异常退出都调用)。
     *
     * @param joinPoint joinPoint
     */
//    @After("paramsCheck()")
//    public void after(JoinPoint joinPoint) {
//        System.out.println("do after。");
//    }

    /**
     * 后通知(只有当连接点正常退出时才调用)
     *
     * @param joinPoint joinPoint
     */
//    @AfterReturning(pointcut = "paramsCheck()")
//    public void afterReturning(JoinPoint joinPoint) {
//        System.out.println("do afterReturning。");
//    }

    /**
     * 异常通知,用于拦截层记录异常日志(只有当连接点异常退出时才调用)
     *
     * @param joinPoint joinPoint
     * @param e         e
     */
//    @AfterThrowing(pointcut = "paramsCheck()", throwing = "e")
//    public void afterThrowing(JoinPoint joinPoint, Throwable e) {
//        try {
//            System.out.println("do afterThrowing。");
//        } catch (Exception ex) {
//            logger.error(ExceptionUtils.getFullStackTrace(ex));
//        }
//    }
}

四、@ControllerAdvice 注解标签,定义全局异常捕获,捕获异常后统一处理

@ControllerAdvice 标签可捕获 controller 层抛出的异常,可针对不同异常做不同处理,以下是一种处理方法,实际项目可个性化处理。

package com.example.asynctask.globconf;

import com.alibaba.fastjson.JSON;
import com.example.asynctask.entity.CommonException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * 所有异常报错
     */
    @ExceptionHandler(value = Exception.class)
    public String allExceptionHandler(HttpServletRequest request, Exception exception) {
        String exNm = exception.getClass().getName();
        logger.error("{}请求出现异常:{}", request.getRequestURI(), exNm, exception);

        if (exNm.contains("RpcException")) {

            return "{\"result\":\"13\",\"resultMsg\":\"远程服务调用超时,请稍后重试。\"}";
        } else if (exNm.contains("UnknownHostException") || exNm.contains("IOException")) {

            return "{\"result\":\"15\",\"resultMsg\":\"网络异常,请稍后重试。\"}";
        } else if (exNm.contains("HttpRequestMethodNotSupportedException")) {

            return "{\"result\":\"17\",\"resultMsg\":\"不支持的方法调用\"}";
        } else if (exNm.contains("NumberFormatException") || exNm.contains("JSONException")) {

            return "{\"result\":\"2\",\"resultMsg\":\"参数缺失或格式错误\"}";
        } else if (exception instanceof CommonException) {

            // CommonException 异常处理
            String errCode = ((CommonException) exception).getErrCode();
            String errMsg = ((CommonException) exception).getErrMsg();
            Map<String, String> map = new HashMap<>();
            map.put("result", errCode);
            map.put("resultMsg", errMsg);
            return JSON.toJSONString(map);
        } else {
            return "{\"result\":\"16\",\"resultMsg\":\"服务器开小差了,请稍后重试。\"}";
        }
    }
}

五、测试
controller 层测试方法,方法必填参数设置为 key1 和 key2。

package com.example.asynctask.controller;

import com.example.asynctask.globconf.Check;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

/**
 * @description 自定义注解实现必填参数非空校验
 * @date 2019/4/19 11:20
 */
@RestController
public class TestController {

    @RequestMapping("/test")
    @Check(params = {"key1", "key2"})                 // 若接口无必填参数,则写为 @Check(params = {})  
    public String task2(HttpServletRequest request) throws Exception {
        return "===== test ok =====";
    }
}

5.1 异常流程测试。
当接口不传入 key1 和 key2 参数时,即浏览器输入:http://localhost:8080/test,后端测试结果如下所示:

2019-04-22 10:24:19.280 INFO 1192 — [nio-8080-exec-7] c.e.asynctask.globconf.ParamsAspect : TestController.task2 接收参数: {}
2019-04-22 10:24:19.285 ERROR 1192 — [nio-8080-exec-7] c.e.a.globconf.GlobalExceptionHandler : /test请求出现异常:com.example.asynctask.entity.CommonException
com.example.asynctask.entity.CommonException: 2:参数缺失或格式错误

前端接收到应答数据为:{“result”:“2”,“resultMsg”:“参数缺失或格式错误”}。

5.2 正常流程测试
当接口传入 key1 和 key2 参数时,即浏览器输入:http://localhost:8080/test?key1=val1&&key2=val2,后端测试结果如下所示:

2019-04-22 11:31:28.714 INFO 1192 — [nio-8080-exec-8] c.e.asynctask.globconf.ParamsAspect : TestController.task2 接收参数: {“key1”:“val1”,“key2”:“val2”}
2019-04-22 11:31:28.715 INFO 1192 — [nio-8080-exec-8] c.e.asynctask.globconf.ParamsAspect : TestController.task2 应答数据: “===== test ok =====”; 耗时 1 ms

前端收到正常应答:===== test ok =====。

发布了32 篇原创文章 · 获赞 11 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/piaoranyuji/article/details/89448632