为什么要进行统一功能的处理
Spring统一功能处理的原因主要有以下几点:
1. 提高代码的可维护性、可重用性和可扩展性。在应用程序中,存在一些通用的功能需求,如身份验证、日志记录、异常处理等。这些功能需要在多个地方进行调用和处理。如果每个地方都单独实现这些功能,会导致代码冗余、难以维护和重复劳动。通过统一功能处理的方式,可以将这些通用功能抽取出来,以统一的方式进行处理,从而简化代码结构,提高代码的可读性和可维护性。
2. 降低系统的代码耦合度。 在项目中,无论是controller层、service层还是dao层都可能会有异常发生。如果每个过程都单独处理异常,会导致系统的代码耦合度高,工作量大且不好统一,维护的工作量也很大。因此,将异常处理从各处理过程解耦出来,可以使相关处理过程的功能更加单一,也便于进行异常信息的统一处理和维护。
统一用户登录验证
对于以上问题 Spring 中提供了具体的实现拦截器:HandlerInterceptor,拦截器的实现分为以下两个步骤:
- 创建⾃定义拦截器,实现 HandlerInterceptor 接⼝的 preHandle(执⾏具体⽅法之前的预处理)⽅
法。 - 将⾃定义拦截器加⼊ WebMvcConfigurer 的 addInterceptors ⽅法中。
自定义拦截器
import com.bite.book.constant.Constants;
import com.bite.book.model.UserInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("登录拦截器校验...");
HttpSession session = request.getSession();
UserInfo userInfo = (UserInfo) session.getAttribute(Constants.SESSION_USER_KEY);
if (userInfo!=null && userInfo.getId()>=0){
return true;
}
response.setStatus(401);//401 表示未认证登录
return false;
}
// @Override
// public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// log.info("目标方法执行后");
// }
}
将自定义拦截器添加到系统配置
import com.bite.book.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
import java.util.List;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
private static List<String> excludePath = Arrays.asList("/user/login",
"/css/**",
"/js/**",
"/pic/**",
"/**/*.html",
"/test/**");
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")// /**表示给所有方法添加拦截器
.excludePathPatterns(excludePath);
}
}
其中:
addPathPatterns:表示需要拦截的 URL,“**”表示拦截任意⽅法(也就是所有⽅法)。
excludePathPatterns:表示需要排除的 URL。
拦截器实现原理
正常调用顺序:
加入拦截器后,会在Controeller层之前加一个统一的登录处理:
- 添加拦截器后, 执⾏Controller的⽅法之前, 请求会先被拦截器拦截住. 执⾏ preHandle() ⽅法, 这个⽅法需要返回⼀个布尔类型的值. 如果返回true, 就表⽰放⾏本次操作, 继续访问controller中的⽅法. 如果返回false,则不会放⾏(controller中的⽅法也不会执⾏).
- controller当中的⽅法执⾏完毕后,再回过来执⾏ postHandle() 这个⽅法以及 afterCompletion() ⽅法,执⾏完毕之后,最终给浏览器响应数据
拦截器实现源码分析
在我们执行Spring项目的时候,它首先会执行一个 DispatcherServlet调度器,如下图,下面我们就进行它的源码来分析。
protected void doDispatch(HttpServletRequest request, HttpServletResponse
response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
//1. 获取执⾏链
//遍历所有的 HandlerMapping 找到与请求对应的Handler
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
//2. 获取适配器
//遍历所有的 HandlerAdapter,找到可以处理该 Handler 的
HandlerAdapter
HandlerAdapter ha =
this.getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request,
mappedHandler.getHandler());
if ((new ServletWebRequest(request,
response)).checkNotModified(lastModified) && isGet) {
return;
}
}
//3. 执⾏拦截器preHandle⽅法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
//4. 执⾏⽬标⽅法
mv = ha.handle(processedRequest, response,
mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
//5. 执⾏拦截器postHandle⽅法
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler
dispatch failed", var21);
}
//6. 处理视图, 处理之后执⾏拦截器afterCompletion⽅法
this.processDispatchResult(processedRequest, response,
mappedHandler, mv, (Exception) dispatchException);
} catch (Exception var22) {
//7. 执⾏拦截器afterCompletion⽅法
this.triggerAfterCompletion(processedRequest, response,
mappedHandler, var22);
} catch (Throwable var23) {
this.triggerAfterCompletion(processedRequest, response,
mappedHandler, new NestedServletException("Handler processing failed", var23));
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}
}
}
我们只用关注一小部分即可:
我们再进入applyPreHandle方法的源码:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
}
}
return true;
}
从上述源码可以看出,在 applyPreHandle 中会获取所有的拦截器 HandlerInterceptor 并执⾏拦截器中的 preHandle ⽅法,这样就会咱们前⾯定义的拦截器对应上了。
统一异常处理
import com.bite.book.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ResponseBody
@Slf4j
@ControllerAdvice
public class ErrorHandler {
@ExceptionHandler(NullPointerException.class)
public Result excption(NullPointerException e){
log.error("发生异常,e:",e);
return Result.fail("NullPointerException异常, 请联系管理员");
}
@ExceptionHandler(Exception.class)
public Result excption(Exception e){
log.error("发生异常,e:",e);
return Result.fail("内部错误");
}
}
其中,方法名和返回值可以自定义,其中最重要的是 @ExceptionHandler(Exception.class) 注解。
由于在上述统一异常处理中,我们只处理了空指针的异常处理,在实际中,我们一般会加一个保底的异常处理,即当我们定义的异常无法捕获该异常时,我们有所有异常的父类Exception来捕获。
统一数据格式返回
import com.bite.book.enums.ResultCode;
import lombok.Data;
@Data
public class Result<T> {
/**
* 业务状态码
*/
private ResultCode code; //0-成功 -1 失败 -2 未登录
/**
* 错误信息
*/
private String errMsg;
/**
* 数据
*/
private T data;
public static <T> Result<T> success(T data){
Result result = new Result();
result.setCode(ResultCode.SUCCESS);
result.setErrMsg("");
result.setData(data);
return result;
}
public static <T> Result<T> fail(String errMsg){
Result result = new Result();
result.setCode(ResultCode.FAIL);
result.setErrMsg(errMsg);
result.setData(null);
return result;
}
public static <T> Result<T> fail(String errMsg,Object data){
Result result = new Result();
result.setCode(ResultCode.FAIL);
result.setErrMsg(errMsg);
result.setData(data);
return result;
}
public static <T> Result<T> unlogin(){
Result result = new Result();
result.setCode(ResultCode.UNLOGIN);
result.setErrMsg("用户未登录");
result.setData(null);
return result;
}
}
为什么需要统一的数据返回?
- 方便前端程序员更好的接收和解析后端返回的数据。
- 降低前后端程序员沟通的成本,按照某个特定的格式返回就可以了。
一般我们统一返回的数据格式如下: - 状态码:用来标识执行成功或失败的状态信息
- 消息:用来描述请求的具体消息
- 数据:包括请求的数据消息
统一数据格式返回的实现
一般我们通过@ControllerAdvice+ResponseBodyAdvice的方式实现,具体如下:
定义一个类加上@ControllerAdvice注解,并继承ResponseBodyAdvice接口:
import com.bite.book.model.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
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.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//在返回之前, 需要做的事情
//body 是返回的结果
if (body instanceof Result){
return body;
}
if (body instanceof String){
return objectMapper.writeValueAsString(Result.success(body));
}
return Result.success(body);
}
}
当我们继承了这个接口后,我们就需要重写它的上面这两个方法,其中supports返回true才会调用下面的beforeBodyWrite方法。值得注意的是我们需要对String类型的body做单独的处理,因为当我们走下面的步骤时会报错:
报错信息:
HttpMessageConverter
实现。
public AllEncompassingFormHttpMessageConverter() {
if (!shouldIgnoreXml) {
try {
addPartConverter(new SourceHttpMessageConverter<>());
} catch (Error err) {
// Ignore when no TransformerFactory implementation is available
}
if (jaxb2Present && !jackson2XmlPresent) {
addPartConverter(new Jaxb2RootElementHttpMessageConverter());
}
}
if (kotlinSerializationJsonPresent) {
addPartConverter(new KotlinSerializationJsonHttpMessageConverter());
}
if (jackson2Present) {
addPartConverter(new MappingJackson2HttpMessageConverter());
} else if (gsonPresent) {
addPartConverter(new GsonHttpMessageConverter());
} else if (jsonbPresent) {
addPartConverter(new JsonbHttpMessageConverter());
}
if (jackson2XmlPresent && !shouldIgnoreXml) {
addPartConverter(new MappingJackson2XmlHttpMessageConverter());
}
if (jackson2SmilePresent) {
addPartConverter(new MappingJackson2SmileHttpMessageConverter());
}
}
public abstract class AbstractMessageConverterMethodProcessor extends
AbstractMessageConverterMethodArgumentResolver
implements HandlerMethodReturnValueHandler {
//...代码省略
protected <T> void writeWithMessageConverters(@Nullable T value,
MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse
outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException,
HttpMessageNotWritableException {
//...代码省略
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
for (HttpMessageConverter<?> converter : this.messageConverters) {
GenericHttpMessageConverter genericConverter = (converter
instanceof GenericHttpMessageConverter ?
(GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ?
((GenericHttpMessageConverter)
converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
//getAdvice().beforeBodyWrite 执⾏之后, body转换成了Result类型的
结果
body = getAdvice().beforeBodyWrite(body, returnType,
selectedMediaType,
(Class<? extends HttpMessageConverter<?>>)
converter.getClass(),
inputMessage, outputMessage);
if (body != null) {
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn ->
"Writing [" + LogFormatUtils.formatValue(theBody,
!traceOn) + "]");
addContentDispositionHeader(inputMessage, outputMessage);
if (genericConverter != null) {
genericConverter.write(body, targetType,
selectedMediaType, outputMessage);
} else {
//此时cover为StringHttpMessageConverter
((HttpMessageConverter) converter).write(body,
selectedMediaType, outputMessage);
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("Nothing to write: null body");
}
}
return;
}
}
}
//...代码省略
}
//...代码省略
}