1、统一的返回格式封装
大榜:前面,我们讨论了白卷项目的前3个优化事项,接下来我们继续进行优化,主要是下面4个优化项:统一的返回格式封装、统一的Web层全局异常处理器、登录优化、登录认证之Cookie/Session。
小汪:好啊,我们一起讨论学习,共同进步!第一个优化点是统一的返回响应格式封装,感觉在接口数量比较多的情况,才会有很大作用。我一般写后端请求接口,代码是这样的:
/** * 登出接口 * @return */ @ResponseBody // 该注解,表示后端返回的是JSON格式。 @GetMapping("/api/logout") public Result logout() { Subject subject = SecurityUtils.getSubject(); subject.logout(); log.info("成功登出"); return ResultFactory.buildSuccessResult("成功登出"); } 复制代码
你看,这是我写的一个登出接口,使用ResultFactory.buildSuccessResult方法封装得到Result对象,然后返回给前端。假设,我们有100个接口,那就需要编写100次重复的return ResultFactory.buildSuccessResult("成功登出")语句,来封装得到Result对象,返回给前端。
大榜:你这个例子很不错啊。其实,对于前后端分离项目,前端与后端是通过统一的格式进行交互,比如你代码中的Result类。这样的话,就像你说的,有多少个接口,你就需要重复编写对应次数的封装语句,来封装得到Result对象,费时费力啊。
小汪:是啊,那怎么才能不编写这些重复的返回封装语句呢?
大榜:哈哈哈,我们可以使用统一的返回格式封装,这样就可以不用编写重复的封装语句了。代码是下面这样的:
package com.bang.wj.component; import com.bang.wj.entity.enumeration.ErrorCode; import com.bang.wj.exception.GlobalWebExceptionAdvice; import com.bang.wj.exception.ResponseJson; import lombok.extern.slf4j.Slf4j; 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.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import javax.servlet.http.HttpServletRequest; /** * 此处约定:哪个分系统需要使用此返回数据封装的功能,就添加上自己分系统所属的Controller层的包名即可。 * 将Controller层返回的数据,统一封装为ResponseJson,然后返回给前端 * * 注解@ControllerAdvice("com.bang.wj.controller") * // 对controller包中,所有Controller类的HTTP响应都做统一格式封装,封装为ResponseJson * * @author qinxubang * @Date 2021/6/13 12:45 */ @Slf4j @ControllerAdvice(basePackages = {"com.bang.wj.controller2"}) // 当前,我们只对controller2包中的HTTP响应做统一格式封装 public class ResponseJsonAdvice implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { Class<?> declaringClass = returnType.getMethod().getDeclaringClass(); return !declaringClass.isAssignableFrom(GlobalWebExceptionAdvice.class); } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body instanceof ResponseJson) { log.warn("该请求的响应,返回的是ResponseJson,无需再次封装,{}", body); return body; } ResponseJson<Object> responseJson = new ResponseJson<>(ErrorCode.SUCCESS); responseJson.setData(body == null ? "" : body); if (request instanceof HttpServletRequest) { HttpServletRequest httpServletRequest = (HttpServletRequest) request; log.info("请求路径是: {}", httpServletRequest.getRequestURI()); } log.info("将指定包中的HTTP响应,做统一ResponseJson格式封装,请求url:{}", request.getURI()); return responseJson; } } 复制代码
你看,我们编写了一个类ResponseJsonAdvice,实现了Spring的ResponseBodyAdvice接口,重写了的beforeBodyWrite方法,逻辑是这样的:如果返回体body属于我们自定义的ResponseJson格式,就直接返回;否则,添加自定义的状态码,并对body进行封装得到ResponseJson格式。这样,就实现了统一的返回格式封装。需要注意的是,ResponseJsonAdvice类上面有个注解:
@ControllerAdvice(basePackages = {"com.bang.wj.controller2"}) 复制代码
这个注解中,定义了包名称,它的意思是只对com.bang.wj.controller2 包下的类,进行统一返回格式封装,其他包下的类 则不会进行统一格式封装。
小汪:我懂了。我们一般只对Http请求接口进行统一封装,也就是对xxx.controller包下的Controller类进行封装,你这个注解中定义的包名称,没问题啊。这个ResponseJsonAdvice类,如果我拿来用,只需要将@ControllerAdvice注解中的包名称修改为我自己的Http接口对应的包名称就可以了,这样就实现了统一返回格式封装。以后即使1000个接口,我也不用重复编写返回封装语句了,很香很香啊!
2、Web层的全局异常处理器
大榜:哈哈哈,小伙子很稳啊。后端接口数量比较多的情况下,使用Spring的ResponseBodyAdvice接口实现统一的返回格式封装,以后就可以少搬一会儿砖、多喝一杯茶了。
小汪:是啊。那第2个优化是统一的Web层的全局异常处理器,这个在什么需求场景下使用呢?
大榜:一般是后端的Http接口产生异常了,由我们的Web层全局异常器来对异常进行处理。
小汪:我感觉用处不大啊。你看,如果后端接口要是产生异常了,我也可以自己来处理异常,代码是这样的:
@ResponseBody // 该注解,表示后端返回的是JSON格式。 @GetMapping("/api/logout") public Result logout() { Subject subject = SecurityUtils.getSubject(); try { subject.logout(); } catch (Exception e) { log.error("登出的用户名:{};/api/logout产生异常:",subject.getPrincipal().toString(), e); } log.info("成功登出"); return ResultFactory.buildSuccessResult("成功登出"); } 复制代码
代码中通过catch来捕获异常,然后将登出的用户名、请求url打印出来,而且为了便于排故,我还将捕获的异常打印出来了。感觉我自己通过catch来捕获和处理异常也很方便,没有必要使用全局异常处理器啊?
大榜:你这个接口产生的异常,把必要信息和异常都打印出来了,处理得很好。但如果有100个接口呢,你是不是需要自己来写100个异常处理了呢?
小汪:哦哦,是啊。和第一个优化点:使用统一的返回响应格式封装很类似啊,都是在接口比较多的需求场景下使用。那如何实现Web层的全局异常处理器呢?
大榜:其实也不难,因为Spring已经帮我们做好了Web层的全局异常处理器,代码是下面这样的:
package com.bang.wj.exception; import com.bang.wj.component.RepeatReadFilter; import com.bang.wj.entity.enumeration.ErrorCode; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.servlet.http.HttpServletRequest; import java.io.IOException; /** * Web层接口的全局异常处理器 * * 此处加上包名称"com.bang.wj.controller",全局异常处理器则只会扫描该包路径下抛出的异常,就会导致com.bang.wj.controller2下抛出的异常不会被捕获。 * 注解@RestControllerAdvice("com.bang.wj.controller") * * @author qinxubang * @Date 2021/6/12 13:54 */ @Slf4j @RestControllerAdvice public class GlobalWebExceptionAdvice { // 捕获下面的异常后,将请求的url、请求参数写入响应体中,然后返回给前端。 @ExceptionHandler(IllegalStateException.class) public ResponseJson<RequestInfo> illegalStateExceptionHandler(HttpServletRequest request, Exception exception) throws IOException { String stackTrace = ExceptionUtils.getStackTrace(exception); log.error("Web全局处理器处理异常:{}", stackTrace); // 响应Json格式中,包装了请求参数信息; 响应的构造函数中,传入的参数为自定义的错误码ErrorCode ResponseJson<RequestInfo> responseJson = new ResponseJson<>(ErrorCode.SERVER_ERROR); // 给请求实体requestInfo初始化赋值 RequestInfo requestInfo = RequestJsonUtils.getRequestInfo(request); requestInfo.setMessage(exception.getMessage()); // 将导致异常的该次请求url、请求参数、错误信息,存入响应体中 responseJson.setData(requestInfo); retur