作为一个程序猿,无论你在哪家公司工作服务,成规模的团队都有封装公司内部使用的框架,比如REST,dubbo,Redis,Kafka,Job,Log,Util等,对于REST的封装主要需要解决的问题有如下几个:
本小节先处理前4个问题; 当下,springboot异常流行,如果团队内部的框架封装也按照starter的方式,可以轻松的获得starter的优点,无需像之前那样进行xml或者javaBean的配置,直接暴露properties即可,对于团队的协作和效率的提升有极大的帮助。
下面单刀直入,针对核心诉求的问题,进行一一拆解,并使用starter的方式进行封装;
原理主要是基于:ResponseBodyAdvice接口
javadoc文档:
package org.springframework.web.servlet.mvc.method.annotation;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.lang.Nullable;
/**
* Allows customizing the response after the execution of an {@code @ResponseBody}
* or a {@code ResponseEntity} controller method but before the body is written
* with an {@code HttpMessageConverter}.
*
* <p>Implementations may be registered directly with
* {@code RequestMappingHandlerAdapter} and {@code ExceptionHandlerExceptionResolver}
* or more likely annotated with {@code @ControllerAdvice} in which case they
* will be auto-detected by both.
*
* @author Rossen Stoyanchev
* @since 4.1
* @param <T> the body type
*/
public interface ResponseBodyAdvice<T> {
/**
* Whether this component supports the given controller method return type
* and the selected {@code HttpMessageConverter} type.
* @param returnType the return type
* @param converterType the selected converter type
* @return {@code true} if {@link #beforeBodyWrite} should be invoked;
* {@code false} otherwise
*/
boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);
/**
* Invoked after an {@code HttpMessageConverter} is selected and just before
* its write method is invoked.
* @param body the body to be written
* @param returnType the return type of the controller method
* @param selectedContentType the content type selected through content negotiation
* @param selectedConverterType the converter type selected to write to the response
* @param request the current request
* @param response the current response
* @return the body that was passed in or a modified (possibly new) instance
*/
@Nullable
T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response);
}
简单阐述一下要点:
允许定制执行一个控制器的 @ResponseBody 或者一个ResponseEntity 方法后的响应,但是在body被HttpMessageConverter写回之前;
实现可以被RequestHandlerAdapter和ExceptionHanderExceptionResolver 直接注册实现,大部分情况通过注解@ControllerAdvice修饰,在这种情况下,他们会被自动检测到;
代码封装如下:
package com.springx.bootdubbo.starter.rest.core;
import com.springx.bootdubbo.common.bean.RestContextBean;
import com.springx.bootdubbo.common.bean.RestResponseBean;
import com.springx.bootdubbo.common.enums.ErrorCodeMsgEnum;
import com.springx.bootdubbo.common.exception.BaseException;
import com.springx.bootdubbo.common.util.JsonUtil;
import com.springx.bootdubbo.common.util.SystemUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.servlet.http.HttpServletRequest;
import java.util.LinkedHashMap;
import java.util.Objects;
/**
* 作者: carter
* 创建日期: 2019/6/1 上午10:23
* 描述: 统一返回格式,正常情况或者出现异常的时候
*/
@RestControllerAdvice
@Slf4j
@Component
public class ResponseBodyAndExceptionHandleBean implements ResponseBodyAdvice {
public ResponseBodyAndExceptionHandleBean(){
}
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
if (Objects.isNull(returnType)){
return true;
}
return returnType.hasMethodAnnotation(ResponseBody.class)
|| returnType.getMethod().getDeclaringClass().isAnnotationPresent(RestController.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
RestResponseBean restResponse= getResponse(body);
log.info("\n===param_response===\n===requestId==={}\n===json==={}", RestContextBean.getInstance().getRequestId(), JsonUtil.toJson(restResponse));
return restResponse;
}
private RestResponseBean getResponse(Object body) {
if (Objects.isNull(body)){
return RestResponseBean.builder().code(ErrorCodeMsgEnum.SUCCESS.code())
.msg(ErrorCodeMsgEnum.SUCCESS.getMsg())
.build();
}
if (body instanceof RestResponseBean){
RestResponseBean restResponseBean = (RestResponseBean) body;
String msg = I18nUtils.getMessage(restResponseBean.getCode() + "", restResponseBean.getMsg());
return RestResponseBean.builder().code(restResponseBean.getCode())
.msg(msg)
.data(restResponseBean.getData())
.build();
}
if (body instanceof LinkedHashMap) {
LinkedHashMap hashMap = (LinkedHashMap) body;
if (hashMap.containsKey("error") && hashMap.containsKey("path") && hashMap.containsKey("status") && hashMap.containsKey("timestamp")) {
Object status = hashMap.get("status");
Integer code = ErrorCodeMsgEnum.SUCCESS.code();
if (StringUtils.isNumeric(Objects.toString(status))) {
code = Integer.parseInt(Objects.toString(status));
}
return RestResponseBean.builder().code(code)
.msg(String.valueOf(hashMap.get("error")))
.build();
}
}
return RestResponseBean.builder().code(ErrorCodeMsgEnum.SUCCESS.code())
.msg(ErrorCodeMsgEnum.SUCCESS.getMsg())
.data(body)
.build();
}
}
通过转化contoller跑出的异常为封装的统一格式的RestResponseBean对象,然后在写回客户端之前进行统一的格式转换,最后客户端无论是在正常情况还是异常情况下,收到的都是统一的RestResponseBean对象对应的Json数据,前端可以基于该对象做统一的处理,200是正常情况,500是异常情况,还可以扩展可检查异常作为提示信息;
starter的封装:
@Bean
@ConditionalOnProperty(prefix = "rest.config", value = "enabled", havingValue = "true")
public ResponseBodyAndExceptionHandleBean exceptionHandlerBean(){
log.info("===>install exception handler AND response wrapper ");
applicationContext.getBean(DispatcherServlet.class).setThrowExceptionIfNoHandlerFound(true);
return new ResponseBodyAndExceptionHandleBean();
}
所有接口的异常,最终都会反馈到控制器的异常,所以这里对控制器的异常进行统一处理,自定义一个基本异常类,相信工程师也基本上是这样定义的。
package com.springx.bootdubbo.common.bean;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.StringJoiner;
/**
* @author <a href="mailto:505847426@qq.com">carterbrother</a>
* @description Rest统一返回给前端的数据对象
* @date 2019年05月17日 5:31 PM
* @Copyright (c) carterbrother
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RestResponseBean implements Serializable {
/**
* 状态码
*/
private Integer code;
/**
* 返回消息
*/
private String msg;
/**
* 响应数据
*/
private Object data;
@Override
public String toString() {
return new StringJoiner(", ", RestResponseBean.class.getSimpleName() + "[", "]")
.add("code=" + code)
.add("msg='" + msg + "'")
.add("data=" + data)
.toString();
}
}
异常处理代码:
@ExceptionHandler(Exception.class)
@ResponseBody
public RestResponseBean handleException(HttpServletRequest request, Exception e) {
Integer code = ErrorCodeMsgEnum.ERROR.code();
String msg = null;
String exceptionMsg = "";
if (e instanceof BaseException) {
BaseException baseException = (BaseException) e;
code = baseException.getCode();
msg = baseException.getMsg();
exceptionMsg = msg;
} else if (e instanceof NoHandlerFoundException) {
code = ErrorCodeMsgEnum.NOT_FOUND.code();
msg = ErrorCodeMsgEnum.NOT_FOUND.getMsg();
} else {
exceptionMsg = e.getLocalizedMessage();
msg = exceptionMsg;
if (SystemUtil.isOnlineEnv()) {
msg = "系统错误";
}
log.error("\n===param_exception===\n===requestId==={}\n===exception==={}", RestContextBean.getInstance().getRequestId(), e);
}
return RestResponseBean.builder().code(code).msg(msg).build();
}
在自动装配的代码中注意着一句:
applicationContext.getBean(DispatcherServlet.class).setThrowExceptionIfNoHandlerFound(true);
会把404转换成异常抛出来;
这样所有的异常都转换为统一格式的响应对象。
日志的作用不用作过多的强调,开发过程中可以使用调试工具,这非常直观。但是当项目上线之后,你不可能去debug,这个时候你必须依靠日志,依靠日志定位功能问题,分析接口的性能问题,从而快速的定位和解决功能,或者进行各种层级的性能调优。 日志有很多实现方案,这里我选择了一种最简单的,直接使用拦截器。
通过stopWatch,可以在业务代码的任何阶段进行时间的统计和输出。 第一个阶段:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
stopWatch.split();
log.info("\n===param_request==={}", getRequestParam(request, restContext));
}
private String getRequestParam(HttpServletRequest request, RestContextBean restContext) {
Assert.notNull(request, "请求对象request不能为空");
Assert.notNull(restContext, "restContext不能为空");
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("\n===Url===").append(request.getContextPath().concat(request.getRequestURL().toString()));
stringBuilder.append("\n===header==={\n");
final Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
final String headName = headerNames.nextElement();
stringBuilder.append(headName).append("=").append(request.getHeader(headName)).append(",\n");
}
stringBuilder.append("}\n");
stringBuilder.append("===param==={\n");
request.getParameterMap().forEach((paramKey, paramValueArray) -> {
stringBuilder.append(paramKey).append("=").append(Arrays.toString(paramValueArray)).append(",\n");
});
stringBuilder.append("}\n");
stringBuilder.append("===RestContext==={\n").append(Objects.toString(restContext)).append("\n}");
return stringBuilder.toString();
}
第二个阶段:
见通用返回对象的封装;
log.info("\n===param_response===\n===requestId==={}\n===json==={}", RestContextBean.getInstance().getRequestId(), JsonUtil.toJson(restResponse));
第三阶段:
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
final StopWatch stopWatch = RestContextBean.getInstance().getStopWatch();
log.info("\n===param_complete===\n===requestId==={}\n===method==={}\n===useTime==={}", RestContextBean.getInstance().getRequestId(), handler, stopWatch.getTime());
stopWatch.stop();
RestContextBean.clear();
MDC.clear();
}
通过以上3步的处置,可以通过requestId轻松的跟进一个请求的请求参数,响应内容,以及接口的耗时。
通用参数包括requestId,appType,LoginId,appVersion,local等,这些跟业务无关但是很重要的通用参数应该需要下沉在一个跟线程相关的对象的中,并且很容易的获取到。借助TheadLocal技术,可以很好的实现;
在拦截器中,分两个阶段:
package com.springx.bootdubbo.common.bean;
import com.springx.bootdubbo.common.enums.AppTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.StopWatch;
import java.util.Arrays;
import java.util.Objects;
import java.util.stream.Stream;
/**
* @author <a href="mailto:505847426@qq.com">carterbrother</a>
* @description 封装一些请求信息中的公共信息
* @date 2019年05月17日 10:41 AM
* @Copyright (c) carterbrother
*/
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
public class RestContextBean {
/**
* 计时器,计算每一步的时间
*/
private StopWatch stopWatch;
/**
* 请求ID
*/
private String requestId;
/**
* 应用类型,标识系统
*/
private AppTypeEnum appType;
/**
* app的版本号
*/
private String appVersion;
/**
* 用户ID,如果url或者header中有
*/
private Long userId;
/**
* 登录的用户ID,登录才有
*/
private Long loginId;
/**
* 客户端id
*/
private String clientId;
/**
* 操作系统类型
*/
private Integer osType;
/**
* 国家
*/
private String country;
/**
* 省份
*/
private String province;
/**
* 城市
*/
private String city;
/**
* 区域
*/
private String area;
/**
* 语言
*/
private String language;
/**
* 屏幕宽度
*/
private Integer screenWidth;
/**
* 屏幕高度
*/
private Integer screenHeight;
/**
* 登录类型,1普通账号登录 2 微信登录 3 其它登录
*/
private Integer loginType;
/**
* 授权码,登录才有
*/
private String accessToken;
private static final ThreadLocal<RestContextBean> LOCAL = ThreadLocal.withInitial(RestContextBean::new);
/**
* 设置实例
*
* @param restContext
*/
public static void setLocal(RestContextBean restContext) {
LOCAL.set(restContext);
}
/**
* 获取实例
*
* @return
*/
public static RestContextBean getInstance() {
return LOCAL.get();
}
/**
* 清理实例
*/
public static void clear() {
LOCAL.remove();
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("[");
Stream.of(this.getClass().getDeclaredFields()).filter(item -> Arrays.asList("log", "LOCAL").stream().noneMatch(xx -> Objects.equals(item.getName(), xx)))
.forEach(field -> {
field.setAccessible(true);
try {
final Object fieldValue = field.get(this);
if (Objects.nonNull(fieldValue)) {
stringBuilder.append("\n").append(field.getName()).append("=").append(Objects.toString(fieldValue)).append(",");
}
} catch (Exception ex) {
log.error("获取属性失败:{}", ex);
}
});
stringBuilder.append("\n]");
return stringBuilder.toString();
}
public static class KeyDict {
public static final String REQUEST_ID_KEY = "requestId";
public static final String APP_TYPE_KEY = "appType";
public static final String APP_VERSION_KEY = "appVersion";
public static final String USER_ID_KEY = "userId";
public static final String LOGIN_ID_KEY = "loginId";
public static final String CLIENT_ID_KEY = "clientId";
public static final String OS_TYPE_KEY = "osType";
public static final String COUNTRY_KEY = "country";
public static final String PROVINCE_KEY = "province";
public static final String CITY_KEY = "city";
public static final String AREA_KEY = "area";
public static final String LANGUAGE_KEY = "language";
public static final String SCREEN_WIDTH_KEY = "screenWidth";
public static final String SCREEN_HEIGHT_KEY = "screenHeight";
public static final String LOGIN_TYPE_KEY = "loginType";
public static final String ACCESS_TOKEN_KEY = "accessToken";
public static final String REQUEST_URL_KEY = "requestUrl";
public static final String SERVICE_NAME_KEY = "serviceName";
public static final String ENV_KEY = "env";
public static final String STOP_WATCH_KEY = "stopWatch";
}
}
这样在实际的业务代码编写过程中,可以方便的获取到这些下沉的参数。
data.put("restContextBean", RestContextBean.getInstance().getRequestId());
类似静态工具方法一样获取,而这个是线程安全的。
package com.springx.bootdubbo.starter.rest.core;
import com.google.common.base.Defaults;
import com.google.common.base.Strings;
import com.springx.bootdubbo.common.bean.RestContextBean;
import com.springx.bootdubbo.common.enums.AppTypeEnum;
import com.springx.bootdubbo.common.exception.BaseException;
import com.springx.bootdubbo.common.util.RequestUtil;
import com.springx.bootdubbo.starter.rest.config.RestPropertiesConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.slf4j.MDC;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.Assert;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
/**
* @author <a href="mailto:505847426@qq.com">carterbrother</a>
* @description 自定义拦截器,
* 用途:
* 1,填充和清理RestContextBean;
* 2.记录Rest的请求日志和正常响应日志
* 3.执行登录校验
* @date 2019年05月16日 6:15 PM
* @Copyright (c) carterbrother
*/
@Slf4j
public class RestContextInterceptorBean implements HandlerInterceptor, ApplicationContextAware {
private RestPropertiesConfig restPropertiesConfig;
private ApplicationContext applicationContext;
public RestContextInterceptorBean( RestPropertiesConfig restPropertiesConfig) {
this.restPropertiesConfig = restPropertiesConfig;
log.info("===>init RestContextInterceptorBean");
if (Objects.isNull(restPropertiesConfig)){
this.restPropertiesConfig = applicationContext.getBean(RestPropertiesConfig.class);
}
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Integer appTypeInteger = getIntegerValue(request, RestContextBean.KeyDict.APP_TYPE_KEY);
AppTypeEnum appType = Defaults.defaultValue(AppTypeEnum.class);
if (Objects.nonNull(appTypeInteger)) {
appType = AppTypeEnum.fromAppId(appTypeInteger);
}
final String requestId = getStringValue(request, RestContextBean.KeyDict.REQUEST_ID_KEY);
final Long loginId = getLongValue(request, RestContextBean.KeyDict.LOGIN_ID_KEY);
final Long userId = getLongValue(request, RestContextBean.KeyDict.USER_ID_KEY);
MDC.put(RestContextBean.KeyDict.REQUEST_ID_KEY, requestId);
MDC.put(RestContextBean.KeyDict.REQUEST_URL_KEY, request.getContextPath().concat(request.getRequestURI()));
MDC.put(RestContextBean.KeyDict.APP_TYPE_KEY, Objects.isNull(appType) ? "" : appType.name());
MDC.put(RestContextBean.KeyDict.SERVICE_NAME_KEY, System.getProperty(RestContextBean.KeyDict.SERVICE_NAME_KEY));
MDC.put(RestContextBean.KeyDict.ENV_KEY, System.getProperty(RestContextBean.KeyDict.ENV_KEY));
MDC.put(RestContextBean.KeyDict.USER_ID_KEY, Objects.toString(Objects.isNull(loginId) ? userId : loginId, ""));
final RestContextBean restContext = RestContextBean.builder().appType(appType)
.stopWatch(stopWatch)
.requestId(requestId)
.appVersion(getStringValue(request, RestContextBean.KeyDict.APP_VERSION_KEY))
.userId(userId)
.loginId(loginId)
.accessToken(getStringValue(request, RestContextBean.KeyDict.ACCESS_TOKEN_KEY))
.clientId(getStringValue(request, RestContextBean.KeyDict.CLIENT_ID_KEY))
.osType(getIntegerValue(request, RestContextBean.KeyDict.OS_TYPE_KEY))
.country(getStringValue(request, RestContextBean.KeyDict.COUNTRY_KEY))
.province(getStringValue(request, RestContextBean.KeyDict.PROVINCE_KEY))
.city(getStringValue(request, RestContextBean.KeyDict.CITY_KEY))
.area(getStringValue(request, RestContextBean.KeyDict.AREA_KEY))
.language(getStringValue(request, RestContextBean.KeyDict.LANGUAGE_KEY))
.screenHeight(getIntegerValue(request, RestContextBean.KeyDict.SCREEN_HEIGHT_KEY))
.screenWidth(getIntegerValue(request, RestContextBean.KeyDict.SCREEN_WIDTH_KEY))
.loginType(getIntegerValue(request, RestContextBean.KeyDict.LOGIN_TYPE_KEY))
.build();
RestContextBean.setLocal(restContext);
stopWatch.split();
log.info("\n===param_request==={}", getRequestParam(request, restContext));
if (Strings.isNullOrEmpty(requestId)) {
throw new BaseException("requestId不能为空");
}
//识别@LoginIgnore注解,如果没有,进行登录校验
final String loginCheckApiFullClassName = restPropertiesConfig.getLoginCheckApi();
// LoginCheckApi loginCheckApi = applicationContext.getBean(loginCheckApiFullClassName, LoginCheckApi.class);
//识别@PowerCheck注解,如果没有,跳过,如果有,校验功能权限
//识别@PowerCheck注解是否需要获取数据权限SQL,如果没有,结束,如果有,则获取数据权限转换的sql语句
return true;
}
private String getRequestParam(HttpServletRequest request, RestContextBean restContext) {
Assert.notNull(request, "请求对象request不能为空");
Assert.notNull(restContext, "restContext不能为空");
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("\n===Url===").append(request.getContextPath().concat(request.getRequestURL().toString()));
stringBuilder.append("\n===header==={\n");
final Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
final String headName = headerNames.nextElement();
stringBuilder.append(headName).append("=").append(request.getHeader(headName)).append(",\n");
}
stringBuilder.append("}\n");
stringBuilder.append("===param==={\n");
request.getParameterMap().forEach((paramKey, paramValueArray) -> {
stringBuilder.append(paramKey).append("=").append(Arrays.toString(paramValueArray)).append(",\n");
});
stringBuilder.append("}\n");
stringBuilder.append("===RestContext==={\n").append(Objects.toString(restContext)).append("\n}");
return stringBuilder.toString();
}
private String getStringValue(HttpServletRequest request, String appType) {
String value = request.getHeader(appType);
if (StringUtils.isEmpty(value)) {
value = request.getParameter(appType);
}
return value;
}
private Long getLongValue(HttpServletRequest request, String paramName) {
String paramStringValue = getStringValue(request, paramName);
Long longValue = null;
if (StringUtils.isNumeric(paramStringValue)) {
try {
longValue = Long.parseLong(paramStringValue);
} catch (NumberFormatException e) {
log.error("转换为长整数失败,{} ", paramName, e);
}
}
return longValue;
}
private Integer getIntegerValue(HttpServletRequest request, String paramName) {
Long longValue = getLongValue(request, paramName);
if (Objects.isNull(longValue)) {
return null;
}
return longValue.intValue();
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
Map<String, String> headMap = new HashMap<>(4);
headMap.put("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
headMap.put("Access-Control-Allow-Headers", "Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type");
headMap.put("Access-Control-Allow-Credentials", "true");
headMap.put("Access-Control-Allow-Origin", "*");
String refererUrl = RequestUtil.parseRequestOrigin(request.getHeader("Referer"), request.getHeader("Origin"));
if (!Strings.isNullOrEmpty(refererUrl)) {
headMap.put("Access-Control-Allow-Origin", refererUrl);
}
headMap.forEach((key, value) -> response.addHeader(key, value));
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
final StopWatch stopWatch = RestContextBean.getInstance().getStopWatch();
log.info("\n===param_complete===\n===requestId==={}\n===method==={}\n===useTime==={}", RestContextBean.getInstance().getRequestId(), handler, stopWatch.getTime());
stopWatch.stop();
RestContextBean.clear();
MDC.clear();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext =applicationContext ;
}
}
通过starter方式封装了小团队常见的REST框架,解决了几个痛点;
封装点 | 爽点 |
---|---|
统一格式返回 | 前后端分离,加速前后端小组的协作效率 |
统一异常处理 | 前后端分离,加速前后端小组的协作效率,统一处理异常,进行预警 |
跟踪日志 | 快速定位生产的功能问题,性能问题并解决 |
通用参数下沉 | 简化业务接口设计,方便适应变化 |
经过这4个小点的设计,解决了大部分的前后端交互和后期运维问题。
登录的设计后面小节单独成文。
代码地址: https://github.com/carterbrother/bootdubbo/tree/master/springx-libs/starters/rest-spring-boot-starter
转载注明出处。