Spring 统一功能处理

发布于:2025-09-11 ⋅ 阅读:(14) ⋅ 点赞:(0)

Spring 统一功能处理



拦截器

  • 可以统一拦截所有请求

拦截器快速入门

什么是拦截器?

  • 拦截器是Spring框架中核心功能之一,主要用于拦截用户请求,在指定的方法前后,根据业务需要执行预先设定的代码
  • 也就是说,允许开发人员提前预定义一些逻辑,在用户的请求响应前后执行。也可以在用户请求前阻止其执行

在拦截器当中,开发人员可以在应用程序中做一些通用性的操作,比如通过拦截器来拦截前端发来的请求,判断Session中是否有登录用户的信息。如果有就可以放行,如果没有就进行拦截

拦截器使用

拦截器的使用步骤分为两步:

  1. 定义拦截器
  2. 注册配置拦截器

自定义拦截器:实现HandlerInterceptor接口,并重写其所有方法

@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("LoginInterceptor 目标方法执行前执行...");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("LoginInterceptor 目标方法执行后执行");
    }

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("LoginInterceptor 视图渲染完毕后执行,最后执行");
    }
}
  • preHandle()方法:目标方法执行前执行。返回true:继续执行后续操作;返回false:中断后续操作。
  • postHandle()方法:目标方法执行后执行
  • afterCompletion()方法:视图渲染完毕后执行,最后执行(后端开发现在几乎不涉及视图,暂不了解)

注册配置拦截器:实现WebMvcConfigurer接口,并重写addInterceptors方法

@Configuration
public class WebConfig implements WebMvcConfigurer {
    //自定义的拦截器对象
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册自定义拦截器对象
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**");//设置拦截器拦截的请求路径(/** 表示拦截所有请求)
    }
}

启动服务,试试访问任意请求,观察后端日志

拦截器详解

接下来我们介绍一下拦截器的使用细节,我们主要介绍两个部分:

  1. 拦截路径的配置
  2. 拦截器实现原理

拦截路径配置

拦截路径是指我们定义的这个拦截器,对哪些请求生效

我们在注册配置拦截器的时候,通过 addPathPatterns() 方法指定要拦截哪些请求。也可以通过 excludePathPatterns() 指定不拦截哪些请求

上述代码中,我们配置的是 / **,表示拦截所有的请求

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor).addPathPatterns("/**")
                .excludePathPatterns("/**/*.html",
                        "/pic/**",
                        "/js/**",
                        "/css/**",
                        "/blog-editormd/**",
                        "/user/login");

    }
}

在拦截器中除了可以设置 / ** 拦截所有资源外,还有一些常见拦截路径设置:

拦截路径 含义 举例
/* 一级路径 能匹配 /user/book/login,不能匹配 /user/login
/** 任意级路径 能匹配 /user/user/login/user/reg
/book/* /book下的一级路径 能匹配 /book/addBook,不能匹配 /book/addBook/1/book
/book/** /book下的任意级路径 能匹配 /book/book/addBook/book/addBook/2,不能匹配 /user/login

拦截器执行流程

正常流程:
在这里插入图片描述
使用拦截器的流程:
在这里插入图片描述

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

适配器模式

适配器的定义

  • 适配器模式,也叫包装器模式,将一个类的接口,转换成客户期望的另一个接口,适配器让原本接口不兼容的类可以合作无间
  • 简单来说就是目标类不能直接使用,通过一个新类进行包装一下,适配调用方使用,把两个不兼容的接口通过一定的方式使之兼容

举个例子:(转接头)

在这里插入图片描述

适配器模式角色

  • Target: 目标接口 (可以是抽象类或接口),客户希望直接用的接口
  • Adaptee: 适配者,但是与Target不兼容
  • Adapter: 适配器类,此模式的核心.通过继承或者引用适配者的对象,把适配者转为目标接口
  • client: 需要使用适配器的对象

适配器模式的实现

场景:前面学习的slf4j就使用了适配器模式,slf4j提供了一系列打印日志的api,底层调用的是log4j或者logback来打日志,我们作为调用者,只需要调用slf4j的api就行了。

/**
 * slf4j接口
 */
interface Slf4jApi{
    void log(String message);
}

/**
 * log4j 接口
 */
class Log4j{
    void log4jLog(String message){
        System.out.println("Log4j打印:"+message);
    }
}

/**
 * slf4j和log4j适配器
 */
class Slf4jLog4jAdapter implements Slf4jApi{
    private Log4j log4j;

    public Slf4jLog4jAdapter(Log4j log4j) {
        this.log4j = log4j;
    }

    @Override
    public void log(String message) {
        log4j.log4jLog(message);
    }
}

/**
 * 客户端调用
 */
public class Slf4jDemo {
    public static void main(String[] args) {
        Slf4jApi slf4jApi = new Slf4jLog4jAdapter(new Log4j());
        slf4jApi.log("使用slf4j打印日志");
    }
}

可以看出,我们不需要改变log4j的api,只需要通过适配器转换下,就可以更换日志框架,保障系统的平稳运行。

适配器模式的实现并不在slf4j-core中(只定义了Logger),具体实现是在针对log4j的桥接器项目slf4j-log4j12中

  • 设计模式的使用非常灵活,一个项目中通常会含有多种设计模式

统一数据返回格式

  • 对后端数据进行封装,返回给前端

快速入门

  1. 实现ResponseBodyAdvice接口,重写supports 和 beforeBodyWriter 方法
  2. 使用@ControllerAdvice注解
import com.example.demo.model.Result;
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 {

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        return Result.success(body);
    }
}
  • supports方法:判断是否要执行beforeBodyWrite方法。true为执行,false不执行。通过该方法可以选择哪些类或哪些方法的response要进行处理,其他的不进行处理

returnType获取类名和方法名

//获取执行的类
Class<?> declaringClass = returnType.getMethod().getDeclaringClass();
//获取执行的方法
Method method = returnType.getMethod();
  • beforeBodyWrite方法:对response方法进行具体操作处理

注意⚠️:

  • 在使用 ResponseBodyAdvice 统一包装响应时,返回值为 String 类型时可能出现转换异常

解决方案:

import com.example.demo.model.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
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;

@Slf4j
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    private static ObjectMapper mapper = new 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) {
        // 如果返回结果为String类型,使用SpringBoot内置提供的Jackson来实现信息的序列化
        if (body instanceof String) {
            return mapper.writeValueAsString(Result.success(body));
        }
        return Result.success(body);
    }
}

原因分析:

  • SpringMVC 默认会注册一些自带的 HttpMessageConverter(从先后顺序排列分别为 ByteArrayHttpMessageConverter,StringHttpMessageConverter,SourceHttpMessageConverter,AllEncompassingFormHttpMessageConverter
  • 其中AllEncompassingFormHttpMessageConverter 会根据项目依赖情况 添加对应的 HttpMessageConverter
  • 在依赖中引入 jackson 包后,容器会把MappingJackson2HttpMessageConverter 自动注册到 messageConverters 链的末尾
  • Spring 会根据返回的数据类型,从 messageConverters 链选择合适的 HttpMessageConverter
  • 当返回的数据是非字符串时,使用的 MappingJackson2HttpMessageConverter 写入返回对象
  • 当返回的数据是字符串时,StringHttpMessageConverter 会先被遍历到,这时会认为 StringHttpMessageConverter 可以使用
  • ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage) 的处理中,调用父类的 write 方法
  • 由于 StringHttpMessageConverter 重写了 addDefaultHeaders 方法,所以会执行子类的方法
  • 然而子类 StringHttpMessageConverteraddDefaultHeaders 方法定义接收参数为 String,此时为 Result 类型,所以出现类型不匹配 "Result cannot be cast to java.lang.String" 的异常

优点

  1. 方便前端程序员更好的接收和解析后端数据接口返回的数据
  2. 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就可以了,因为所有接口都是这样返回的
  3. 有利于项目统一数据的维护和修改
  4. 有利于后端技术部门的统一规范的标准制定,不会出现稀奇古怪的返回内容

统一异常处理

  1. 使用@ControllerAdvice
  2. 使用@ExceptionHandler

注意⚠️:

  • 假如是返回数据,需要加上@ResponseBody注解

具体代码如下:

import com.example.demo.model.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice
@ResponseBody
public class ErrorAdvice {

    @ExceptionHandler
    public Object handler(Exception e) {
        return Result.fail(e.getMessage());
    }
}

以上代码表示,如果代码出现Exception异常(包括Exception的子类),就返回一个 Result的对象,Result对象的设置参考 Result.fail(e.getMessage())

public static Result fail(String msg) {
    Result result = new Result();
    result.setStatus(ResultStatus.FAIL);
    result.setErrorMessage(msg);
    result.setData("");
    return result;
}

我们可以针对不同的异常,返回不同的结果

import com.example.demo.model.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ResponseBody
@ControllerAdvice
public class ErrorAdvice {

    @ExceptionHandler
    public Object handler(Exception e) {
        return Result.fail(e.getMessage());
    }

    @ExceptionHandler
    public Object handler(NullPointerException e) {
        return Result.fail("发生NullPointerException:" + e.getMessage());
    }

    @ExceptionHandler
    public Object handler(ArithmeticException e) {
        return Result.fail("发生ArithmeticException:" + e.getMessage());
    }
}

@ControllerAdvice 源码分析

统一数据返回和统一异常都是基于 @ControllerAdvice 注解来实现的,通过分析 @ControllerAdvice 的源码,可以知道它们的执行流程

点击 @ControllerAdvice 实现源码如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
    @AliasFor("basePackages")
    String[] value() default {};

    @AliasFor("value")
    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

    Class<?>[] assignableTypes() default {};

    Class<? extends Annotation>[] annotations() default {};
}

从上述源码可以看出 @ControllerAdvice 派生于 @Component 组件,这也就是为什么没有五大注解,ControllerAdvice 就生效的原因

下面我们看看 Spring 是怎么实现的,还是从 DispatcherServlet 的代码开始分析,DispatcherServlet 对象在创建时会初始化一系列的对象:

public class DispatcherServlet extends FrameworkServlet {
    //...
    @Override
    protected void onRefresh(ApplicationContext context) {
        initStrategies(context);
    }

    /**
     * Initialize the strategy objects that this servlet uses.
     * <p>May be overridden in subclasses in order to initialize further strategy objects.
     */
    protected void initStrategies(ApplicationContext context) {
        initMultipartResolver(context);
        initLocaleResolver(context);
        initThemeResolver(context);
        initHandlerMappings(context);
        initHandlerAdapters(context);
        initHandlerExceptionResolvers(context);
        initRequestToViewNameTranslator(context);
        initViewResolvers(context);
        initFlashMapManager(context);
    }
    //...
}

对于 @ControllerAdvice 注解,我们重点关注 initHandlerAdapters(context)initHandlerExceptionResolvers(context) 这两个方法。

initHandlerAdapters(context)

initHandlerAdapters(context) 方法会取得所有实现了 HandlerAdapter 接口的 bean 并保存起来,其中有一个类型为 RequestMappingHandlerAdapter 的 bean,这个 bean 就是 @RequestMapping 注解能起作用的关键,这个 bean 在应用启动过程中会获取所有被 @ControllerAdvice 注解标注的 bean 对象,并做进一步处理,关键代码如下:

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {
    //...

    /**
     * 添加ControllerAdvice bean的处理
     */
    private void initControllerAdviceCache() {
        if (getApplicationContext() == null) {
            return;
        }

        //获取所有有被 @ControllerAdvice 注解标注的bean对象
        List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
        List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();

        for (ControllerAdviceBean adviceBean : adviceBeans) {
            Class<?> beanType = adviceBean.getBeanType();
            if (beanType == null) {
                throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
            }
            Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
            if (!attrMethods.isEmpty()) {
                this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
            }
            Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
            if (!binderMethods.isEmpty()) {
                this.initBinderAdviceCache.put(adviceBean, binderMethods);
            }
            if (RequestBodyAdvice.class.isAssignableFrom(beanType) || ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
                requestResponseBodyAdviceBeans.add(adviceBean);
            }
        }

        if (!requestResponseBodyAdviceBeans.isEmpty()) {
            this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
        }

        if (logger.isDebugEnabled()) {
            int modelSize = this.modelAttributeAdviceCache.size();
            int binderSize = this.initBinderAdviceCache.size();
            int reqCount = getBodyAdviceCount(RequestBodyAdvice.class);
            int resCount = getBodyAdviceCount(ResponseBodyAdvice.class);
            if (modelSize == 0 && binderSize == 0 && reqCount == 0 && resCount == 0) {
                logger.debug("ControllerAdvice beans: none");
            } else {
                logger.debug("ControllerAdvice beans: " + modelSize + " @ModelAttribute, " + binderSize + " @InitBinder, " + reqCount + " RequestBodyAdvice, " + resCount + " ResponseBodyAdvice");
            }
        }
        //...
    }
}

这个方法在执行时会查找使用所有的 @ControllerAdvice 类,把 ResponseBodyAdvice 类放在容器中,当发生某个事件时,调用相应的 Advice 方法,比如返回数据前调用统一数据封装。至于 DispatcherServletRequestMappingHandlerAdapter 是如何交互的这就是另一个复杂的话题了

initHandlerExceptionResolvers(context)

接下来看 DispatcherServletinitHandlerExceptionResolvers(context) 方法,这个方法会取得所有实现了 HandlerExceptionResolver 接口的 bean 并保存起来,其中就有一个类型为 ExceptionHandlerExceptionResolver 的 bean,这个 bean 在应用启动过程中会获取所有被 @ControllerAdvice 注解标注的 bean 对象做进一步处理,代码如下:

public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver implements ApplicationContextAware, InitializingBean {
    //...

    private void initExceptionHandlerAdviceCache() {
        if (getApplicationContext() == null) {
            return;
        }
        // 获取所有有被 @ControllerAdvice 注解标注的bean对象
        List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
        for (ControllerAdviceBean adviceBean : adviceBeans) {
            Class<?> beanType = adviceBean.getBeanType();
            if (beanType == null) {
                throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
            }
            ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
            if (resolver.hasExceptionMappings()) {
                this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
            }
            if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
                this.responseBodyAdvice.add(adviceBean);
            }
        }

        if (logger.isDebugEnabled()) {
            int handlerSize = this.exceptionHandlerAdviceCache.size();
            int adviceSize = this.responseBodyAdvice.size();
            if (handlerSize == 0 && adviceSize == 0) {
                logger.debug("ControllerAdvice beans: none");
            } else {
                logger.debug("ControllerAdvice beans: " + handlerSize + " @ExceptionHandler, " + adviceSize + " ResponseBodyAdvice");
            }
        }
        //...
    }
}

当 Controller 抛出异常时,DispatcherServlet 通过 ExceptionHandlerExceptionResolver 来解析异常,而ExceptionHandlerExceptionResolver 又通过 ExceptionHandlerMethodResolver 来解析异常,ExceptionHandlerMethodResolver 最终解析异常找到适用的 @ExceptionHandler 标注的方法是这里:

public class ExceptionHandlerMethodResolver {
    //...
    private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
        List<Class<? extends Throwable>> matches = new ArrayList<>();
        //根据异常类型,查找匹配的异常处理方法:
        //比如initExceptionHandlerAdviceCache会处理两个异常处理方法:
        //handler(Exception e) 和 handler(NullPointerException e)
        for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
            if (mappedException.isAssignableFrom(exceptionType)) {
                matches.add(mappedException);
            }
        }
        //如果是找到多个匹配,就进行排序,找到最使用的方法,排序的规则依据抛出异常相对于声明异常的深度
        //比如抛出的是NullPointerException(继承于RuntimeException),而RuntimeException又继承于Exception
        //相对于handler(Exception e) 声明的NullPointerException深度为0,
        //相对于handler(NullPointerException e)标注的方法会排在前面
        if (!matches.isEmpty()) {
            if (matches.size() > 1) {
                matches.sort(new ExceptionDepthComparator(exceptionType));
            }
            return this.mappedMethods.get(matches.get(0));
        } else {
            return NO_MATCHING_EXCEPTION_HANDLER_METHOD;
        }
    }
    //...
}

网站公告

今日签到

点亮在社区的每一天
去签到