【Spring Boot】之统一功能处理

为什么要进行统一功能的处理

Spring统一功能处理的原因主要有以下几点:

1. 提高代码的可维护性、可重用性和可扩展性。在应用程序中,存在一些通用的功能需求,如身份验证、日志记录、异常处理等。这些功能需要在多个地方进行调用和处理。如果每个地方都单独实现这些功能,会导致代码冗余、难以维护和重复劳动。通过统一功能处理的方式,可以将这些通用功能抽取出来,以统一的方式进行处理,从而简化代码结构,提高代码的可读性和可维护性。

2. 降低系统的代码耦合度。 在项目中,无论是controller层、service层还是dao层都可能会有异常发生。如果每个过程都单独处理异常,会导致系统的代码耦合度高,工作量大且不好统一,维护的工作量也很大。因此,将异常处理从各处理过程解耦出来,可以使相关处理过程的功能更加单一,也便于进行异常信息的统一处理和维护。

统一用户登录验证

对于以上问题 Spring 中提供了具体的实现拦截器:HandlerInterceptor,拦截器的实现分为以下两个步骤:

  1. 创建⾃定义拦截器,实现 HandlerInterceptor 接⼝的 preHandle(执⾏具体⽅法之前的预处理)⽅
    法。
  2. 将⾃定义拦截器加⼊ 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层之前加一个统一的登录处理:

 

  1. 添加拦截器后, 执⾏Controller的⽅法之前, 请求会先被拦截器拦截住. 执⾏ preHandle() ⽅法, 这个⽅法需要返回⼀个布尔类型的值. 如果返回true, 就表⽰放⾏本次操作, 继续访问controller中的⽅法. 如果返回false,则不会放⾏(controller中的⽅法也不会执⾏).
  2. controller当中的⽅法执⾏完毕后,再回过来执⾏ postHandle() 这个⽅法以及 afterCompletion() ⽅法,执⾏完毕之后,最终给浏览器响应数据

拦截器实现源码分析

在我们执行Spring项目的时候,它首先会执行一个 DispatcherServlet调度器,如下图,下面我们就进行它的源码来分析。

当Tomcat启动之后, 有⼀个核⼼的类DispatcherServlet, 它来控制程序的执⾏顺序.
所有请求都会先进到DispatcherServlet,执⾏doDispatch 调度⽅法. 如果有拦截器, 会先执⾏拦截器preHandle() ⽅法的代码, 如果 preHandle() 返回true, 继续访问controller中的⽅法. controller
当中的⽅法执⾏完毕后,再回过来执⾏ postHandle() afterCompletion() ,返回DispatcherServlet, 最终给浏览器响应数据.
DispatcherServlet 接收到请求后, 执⾏doDispatch 调度⽅法, 再将请求转给Controller.
我们来看doDispatch ⽅法的具体实现
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 ⽅法,这样就会咱们前⾯定义的拦截器对应上了。

统一异常处理

统⼀异常处理使⽤的是  @ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执⾏某个通知,也就是执⾏某个⽅法事件,具体实现代码如下:
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做单独的处理,因为当我们走下面的步骤时会报错:
报错信息:

原因分析:
SpringMVC默认会注册⼀些⾃带的 HttpMessageConverter 用于在 HTTP 请求和响应中读写数据。Spring MVC 默认注册了一系列的  HttpMessageConverter 实现。
(从先后顺序排列分别为 ByteArrayHttpMessageConverter , StringHttpMessageConverter , SourceHttpMessageConverter , SourceHttpMessageConverter , AllEncompassingFormHttpMessageConverter )
其中AllEncompassingFormHttpMessageConverter 会根据项⽬依赖情况 添加对应的
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());
        }
    }
在依赖中引⼊jackson包后,容器会把 MappingJackson2HttpMessageConverter ⾃动注册到
messageConverters 链的末尾.
Spring会根据返回的数据类型, 从 messageConverters 选择合适的 HttpMessageConverter .
当返回的数据是⾮字符串时, 使⽤的 MappingJackson2HttpMessageConverter 写⼊返回对象.
当返回的数据是字符串时, StringHttpMessageConverter 会先被遍历到,这时会认为
StringHttpMessageConverter 可以使⽤.
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;
                    }
                }
            }
            //...代码省略

        }
        //...代码省略
    }
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage) 的处理中, 调⽤⽗类的write⽅法
由于 StringHttpMessageConverter 重写了addDefaultHeaders⽅法, 所以会执⾏⼦类的⽅法
然⽽⼦类 StringHttpMessageConverter 的addDefaultHeaders⽅法定义接收参数为String, 此
时t为Result类型, 所以出现类型不匹配"Result cannot be cast to java.lang.String"的异常

 

  • 26
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值