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 =====。