Spring统一功能处理

主要总结了一下内容:

1.掌握拦截器的使用及其原理

2.学习统一数据返回格式和统一异常的操作

一.拦截器

1.拦截器快速入门

1.什么是拦截器?

拦截器是Spring框架提供的核心功能之一,主要用来拦截用户的请求,在指定方法前后,根据业务需要执行预先设定的代码.

2.拦截器的基本使用:

1.定义拦截器

2.注册配置拦截器

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

复制代码
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("LoginInterceptor 目标方法执行前执行");
        return false;
    }

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

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        log.info("LoginInterceptor 视图渲染完毕后执行");
    }
}

preHandle()方法:目标方法执行前执行. 返回true:继续执行后续操作;返回false:中断后续操作.

postHandle()方法:目标方法执行后执行

afterCompletion()方法:视图渲染完毕后执行,最后执行(后端开发现在几乎不涉及视图,暂不了解

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

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册自定义拦截器对象
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**");
    }
}

2.拦截器详解

拦截器的使用细节我们主要了解两个部分:

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

1.拦截器的拦截路径配置

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

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

2.拦截器的实现原理

拦截器的执行流程

正常的调用顺序

有了拦截器之后,会在调用controller之前进行相应的业务处理,执行流程如下图

  1. 添加拦截器后,执行Controller的方法之前,请求会先被拦截器拦截住.执行 preHandle()方法,这个方法需要返回一个布尔类型的值.如果返回true,就表示放行本次操作,继续访问controller中的方法.如果返回false,则不会放行(controller中的方法也不会执行).

  2. controller当中的方法执行完毕后,再回过来执行postHandle()这个方法以及afterCompletion()方法,执行完毕之后,最终给浏览器响应数据.

3.DispatcherServlet源码分析

观察我们的服务启动日志:

当Tomcat启动之后,有一个核心的类DispatcherServlet,他来控制程序的执行顺序

所有的请求都会先进到DIspatcherServlet,执行doDispatch调度方法,如果有拦截器,会先执行拦截器preHandle()方法的代码大,如果preHandle()方法返回true,继续访问controller中的方法,controller中的方法执行完毕之后,再回过来执行postHandle()和afterCompleion(),返回给DispatcherServlet,最终给浏览器响应数据

1.初始化(了解)

DispatcherServlet的初始化方法init()在其父类HttpServletBean中实现的

主要作用是加载web.xml中DispatcherServlet的配置,并调用子类的初始化

web.xml是web项目的配置文件,一般的web工程都会用到web.xml来配置,主要Listener,Filter,Servlet等,Spring框架从3.1版本开始支持Servlet3.0,并且从3.2版本开始通过配置DispatcherServlet,实现不再使用web.xml

init()具体代码如下

java 复制代码
public final void init() throws ServletException {
// ServletConfigPropertyValues 是静态内部类,使用 ServletConfig 获取web.xml 中配置的参数
        PropertyValues pvs = new ServletConfigPropertyValues(this.getServletConfig(), this.requiredProperties);
        if (!pvs.isEmpty()) {
            try {
            //使用BeanWrapper来构造DIspatcherServlet
                BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
                ResourceLoader resourceLoader = new ServletContextResourceLoader(this.getServletContext());
                bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.getEnvironment()));
                this.initBeanWrapper(bw);
                bw.setPropertyValues(pvs, true);
            } catch (BeansException var4) {
                if (this.logger.isErrorEnabled()) {
                    this.logger.error("Failed to set bean properties on servlet '" + this.getServletName() + "'", var4);
                }

                throw var4;
            }
        }
         //让子类实现的方法,这种在父类定义在子类实现的方式叫做模板方法模式
        this.initServletBean();
    }

在 HttpServletBean中init()调用了initServletBean(),他是在FrameworkServlet类中的实现的,主要作用是建立webApplicationContext容器(有时候也叫做上下文),并加载SpringMVC配置文件中定义的Bean到该容器中,最后该容器添加到ServletContext中,下面是initServletBean()的具体代码

java 复制代码
@Override
	protected final void initServletBean() throws ServletException {
		getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
		if (logger.isInfoEnabled()) {
			logger.info("Initializing Servlet '" + getServletName() + "'");
		}
		long startTime = System.currentTimeMillis();

		try {
            //创建ApplicationContext容器
			this.webApplicationContext = initWebApplicationContext();
			initFrameworkServlet();
		}
		catch (ServletException | RuntimeException ex) {
			logger.error("Context initialization failed", ex);
			throw ex;
		}

		if (logger.isDebugEnabled()) {
			String value = this.enableLoggingRequestDetails ?
					"shown which may lead to unsafe logging of potentially sensitive data" :
					"masked to prevent unsafe logging of potentially sensitive data";
			logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
					"': request parameters and headers will be " + value);
		}

		if (logger.isInfoEnabled()) {
			logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
		}
	}

此处打印出来的日志,也正是控制台打印出来的日志

源码追踪技巧:

1.在阅读框架源码的时候,一定要抓住关键点,找到核心流程

2.切忌从头到尾一行一行代码去看,一个方法的去研究,一定要找到关键流程,抓住关键点,先在宏观上

对整个流程或者整个原理有一个认识,有精力再去研究其中的细节.

初始化web容器的过程中,会通过onRefresh来初始化SpringMVC的容器

在initStrategies()中进行9大组件的初始化,如果没有配置相应的组件,就使用默认定义的组件

2.处理请求(核心)

DispatcherServlet接收到请求后,执行doDispatch调度方法,再将请求转发给controller

我们来看doDistpatch方法的具体实现

java 复制代码
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		boolean multipartRequestParsed = false;

		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

		try {
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				// 获取执行链
                //遍历所有的HandlerMapping找到与请求相应的Handler
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

				// 获取适配器
                //遍历所有的HandlerAdapter,找到可以处理该Hander的HandlerAdapter
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				// Process last-modified header, if supported by the handler.
				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.执行拦截器preHandler方法
				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}

				// 4.执行目标方法
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}
				applyDefaultViewName(processedRequest, mv);
                 //5.执行拦截器postHandle方法
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				dispatchException = new ServletException("Handler dispatch failed: " + err, err);
			}
                //6.处理视图,处理之后执行拦截器afterCompletion方法
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		catch (Exception ex) {
            //7.执行拦截器afterCompletion方法
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		}
		catch (Throwable err) {
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new ServletException("Handler processing failed: " + err, err));
		}
		finally {
			if (asyncManager.isConcurrentHandlingStarted()) {
				// Instead of postHandle and afterCompletion
				if (mappedHandler != null) {
					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				}
				asyncManager.setMultipartRequestParsed(multipartRequestParsed);
			}
			else {
				// Clean up any resources used by a multipart request.
				if (multipartRequestParsed || asyncManager.isMultipartRequestParsed()) {
					cleanupMultipart(processedRequest);
				}
			}
		}
	}

HandlerAdapter 在Spring MVC中使用了适配器模式,下面详细再介绍

适配器模式,也叫包装器模式.简单来说就是目标类不能直接使用,通过一个新类进行包装一下,适配

调用方使用.

把两个不兼容的接口通过一定的方式使之兼容.

HandlerAdapter 主要用于支持不同类型的处理器(如Controller、HttpRequestHandler或者Servlet等),让它们能够适配统一的请求处理流程。这样,Spring MVC可以通过一个统一的接口来处理来自各种处理器的请求.

从上述源码我们可以看出在执行controller之前,会先调用预处理方法applyPreHandle,而applyPreHandle方法实现的源码如下:

java 复制代码
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
		for (int i = 0; i < this.interceptorList.size(); i++) {
            //获取项目中使用的HandlerInterceptor
			HandlerInterceptor interceptor = this.interceptorList.get(i);
			if (!interceptor.preHandle(request, response, this.handler)) {
				triggerAfterCompletion(request, response, null);
				return false;
			}
			this.interceptorIndex = i;
		}
		return true;
	}

在applyPreHandle 中会获取所有的拦截器Handler Interceptor,并执行拦截器中的preHandle方法,这样就会咱们前面定义的拦截器对应上了

如果拦截器返回true,整个发放就返回true,继续执行后逻辑处理

如果返回false,则中断后续操作

3.适配器模式

HandlerAdapter在SpringMVC中使用了适配器模式

适配器模式定义

适配器模式也叫包装器模式。将一个类的接口,转换成客户期望的另一个接口,适配器让原本接口不兼容的类可以合作无间。

简单来说,就是目标类不能直接使用,通过一个新类进行包装一下,适配调用方法使用,把两个不兼容的接口通过一定的方式使之兼容

适配器模式角色

Target:目标接口(可以是抽象类或接口),客户希望直接用的接口

Adaptee:适配者,但是与target不兼容

Adapter:适配器类,此模式的核心,通过继承或者引用适配器的对象,把适配器转为目标接口

client:需要使用适配器的对象

适配器模式的实现

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

java 复制代码
/**
 * slf4j接口
 */
interface  Slf4jApi{
    void log(String message);
}

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

/**
 * slf4j和log4j的适配器
 */
class Slf4jLog4Adapter implements Slf4jApi{
    private Log4j log4j;
    public Slf4jLog4Adapter(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 Slf4jLog4Adapter(new Log4j());
        slf4jApi.log("使用log4j打印日志");
    }
}

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

适配器模式的应用场景:

一般来说,适配器模式可以看作一种"补偿 模式",用来补救设计上的缺陷,应用这种模式一般是"无奈之举",如果在设计初期,我们就能协调规避接口不兼容的问题,就不选哟使用适配器模式了

所以适配器模式更多的应用场景主要是对正在运行的代码进行改造,并且希望可以服用原有代码实现新的功能,比如版本升级等。

二.统一数据返回格式

统一的数据返回格式使用@ControllerAdvice和ResponseBodyAdvice的方式实现@ControllerAdvice表示控制器通知类添加类ResponseAdvice,实现ResponseBodyAdvice接口,并且类上添加@ControllerAdvice注解

java 复制代码
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return false;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        return null;
    }
}

supports方法:判断是否要执行beforeBodyWrite方法,true执行,false不执行,通过该方法可以选择哪些类或哪些方法的response要进行处理,其他的不进行处理

beforeBodyWrite:对response方法进行具体操作处理

然后在测试的过程中,我们会发现当返回结果为string类型时会发生错误"Result cannot be cast to java.lang.string

这一现象的原因分析:

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方法,所以会执行子类的方法

然而子类 StringHttpMessageConverter 的addDefaultHeaders方法定义接收参数为String,此

时t为Result类型,所以出现类型不匹配"Result cannot be cast to java.lang.String"的异常

统一结果返回的优点:

1.方便前端更好的接受和解析后端返回的数据

2.降低前后端沟通成本

3.有利于项目统一数据的维护和修改

4.有利于后端技术部门的统一规范的标准指定

三.统一异常处理

统一异常处理使用的是@ControllerAdvice+@ExceptionHandler来实现的,

@ControllerAdvice表示控制器通知类,@Exception Handler是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法事件

@RestControllerAdvice = @ControllerAdvice + @ResponseBody

相关推荐
某鹏1 小时前
网关选型:Go、Java、Graal 谁都不能完胜
后端
用户0247836530971 小时前
从屎山到防线:14 行状态迁移表,给任务系统装上防瞎跳闸门
后端
CQU_JIAKE2 小时前
11.13[体系结构】指标,五级流水线,八级流水线,虚实地址思考
后端
Flynn Liang2 小时前
可观测性与成本治理:一次对话的 17 个观测点
java·langchain·可观测性·micrometer·langchain4j·token 成本·llm 监控
金銀銅鐵2 小时前
带您看懂字节码指令(三):同一行加法,为什么有四种指令
java·jvm
panbook3 小时前
从0到1实现一个分布式时序数据库(三):主从复制、故障检测与切换
后端
上线再说3 小时前
图片从 8.2MB 压到 296KB:我把小程序图片链路的账单拆了一遍
后端
dpharness4 小时前
我原以为 MCP 装完就能用,直到工具列表是空的
后端
dpharness4 小时前
153 个 MCP 服务,我把工具数一个个数了一遍
后端