当前位置: 首页 > news >正文

SpringBoot | 拦截器 | 统一数据返回格式 | 统一异常处理 | 适配器模式

拦截器

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

也就是说, 允许开发人员提前预定义一些逻辑, 在用户的请求响应前后执行. 也可以在用户请求前阻止其执行.

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

基本使用

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

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

package com.example.booksmanagementsystem.interceptor;import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info("LoginInterceptor 目标方法执行前执行...");return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);log.info("LoginInterceptor 目标方法执行后执行");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {HandlerInterceptor.super.afterCompletion(request, response, handler, ex);log.info("LoginInterceptor 视图渲染完毕后执行,最后执行");}
}
  1. preHandle()方法:目标方法执行前执行. 返回true: 继续执行后续操作; 返回false: 中断后续操作.
  2. postHandle()方法:目标方法执行后执行
  3. afterCompletion()方法:视图渲染完毕后执行,最后执行(后端开发现在几乎不涉及视图, 暂不了解)

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

package com.example.booksmanagementsystem.config;import com.example.booksmanagementsystem.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;@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册自定义拦截器对象registry.addInterceptor(loginInterceptor).addPathPatterns("/**"); // 设置拦截器拦截的请求路径(/**表示拦截所有请求}
}

可以看到preHandle 方法执行之后就放行了, 开始执行目标方法, 目标方法执行完成之后执行postHandle和afterCompletion方法.

把preHandle方法的返回值改为false,在观察运行结果

拦截器拦截了请求,没有进行响应

拦截器详解

拦截路径

拦截路径是指我们定义的这个拦截器, 对哪些请求生效.我们在注册配置拦截器的时候, 通过addPathPatterns() 方法指定要拦截哪些请求. 也可以通过excludePathPatterns() 指定不拦截哪些请求.

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

比如用户登录校验, 我们希望可以对除了登录之外所有的路径生效.

package com.example.booksmanagementsystem.config;import com.example.booksmanagementsystem.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;@Configuration
public class WebConfiguration implements WebMvcConfigurer {//自定义的拦截器对象@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象registry.addInterceptor(loginInterceptor).addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表示拦截所有请求).excludePathPatterns("/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

拦截器执行流程

正常的调用顺序

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

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

登录校验

之前实现过一个图书管理系统,只不过是残次品,只有登录功能和查询功能,现在给这个系统实现以下登录校验。


定义拦截器

从session中获取用户信息,如果session中不存在,则返回false,并设置http状态码为401,否则返回true

package com.example.booksmanagementsystem.interceptor;import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info("LoginInterceptor 目标方法执行前执行...");HttpSession session = request.getSession(false);if (session != null && session.getAttribute("userName") != null) {return false;}response.setStatus(401);return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {log.info("LoginInterceptor 目标方法执行后执行");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {log.info("LoginInterceptor 视图渲染完毕后执行,最后执行");}
}

注册配置拦截器

package com.example.booksmanagementsystem.config;import com.example.booksmanagementsystem.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;@Configuration
public class WebConfiguration implements WebMvcConfigurer {//自定义的拦截器对象@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象registry.addInterceptor(loginInterceptor).addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表示拦截所有请求).excludePathPatterns("/user/login").excludePathPatterns("/**/*.js").excludePathPatterns("/**/*.css").excludePathPatterns("/**/*.png").excludePathPatterns("/**/*.html");}
}

也可以改成

package com.example.booksmanagementsystem.config;import com.example.booksmanagementsystem.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 WebConfiguration implements WebMvcConfigurer {//自定义的拦截器对象@Autowiredprivate LoginInterceptor loginInterceptor;private List<String> excludePaths = Arrays.asList("/user/login","/**/*.js","/**/*.css","/**/*.png","/**/*.html");@Overridepublic void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象registry.addInterceptor(loginInterceptor).addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表示拦截所有请求).excludePathPatterns(excludePaths)}
}

未经过认证. 指示身份验证是必需的, 没有提供身份验证或身份验证失败. 如果请求已经包

含授权凭据,那么401状态码表示不接受这些凭据

DispatcherServlet源码分析

在项目启动之后,有一个核心的类DispatcherServlet,它来控制程序的执行顺序,所有的请求都会现进到DispatcherServlet,执行doDispatch调度方法,如果有拦截器,会先执行拦截器preHandle方法的代码,如果preHandle返回true,继续访问controller中的方法,conatroller当中的方法执行完毕之后,再回过来执行postHandle和afterCompletion,返回给DispatcherServlet,最终给浏览器响应数据

初始化

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

/*** Map config parameters onto bean properties of this servlet, and* invoke subclass initialization.* @throws ServletException if bean properties are invalid (or required* properties are missing), or if subclass initialization fails.*/
@Override
public final void init() throws ServletException {// Set bean properties from init parameters.PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);if (!pvs.isEmpty()) {try {BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));initBeanWrapper(bw);bw.setPropertyValues(pvs, true);}catch (BeansException ex) {if (logger.isErrorEnabled()) {logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);}throw ex;}}// Let subclasses do whatever initialization they like.initServletBean();
}

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

/*** Overridden method of {@link HttpServletBean}, invoked after any bean properties* have been set. Creates this servlet's WebApplicationContext.*/
@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 {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");}
}

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

进入这个initWebApplicationcontext方法中,有一个onRefresh方法,在初始化web容器中,会通过onRefresh来初始化SpringMVC容器

/*** Initialize and publish the WebApplicationContext for this servlet.* <p>Delegates to {@link #createWebApplicationContext} for actual creation* of the context. Can be overridden in subclasses.* @return the WebApplicationContext instance* @see #FrameworkServlet(WebApplicationContext)* @see #setContextClass* @see #setContextConfigLocation*/
protected WebApplicationContext initWebApplicationContext() {WebApplicationContext rootContext =WebApplicationContextUtils.getWebApplicationContext(getServletContext());WebApplicationContext wac = null;if (this.webApplicationContext != null) {// A context instance was injected at construction time -> use itwac = this.webApplicationContext;if (wac instanceof ConfigurableWebApplicationContext) {ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;if (!cwac.isActive()) {// The context has not yet been refreshed -> provide services such as// setting the parent context, setting the application context id, etcif (cwac.getParent() == null) {// The context instance was injected without an explicit parent -> set// the root application context (if any; may be null) as the parentcwac.setParent(rootContext);}configureAndRefreshWebApplicationContext(cwac);}}}if (wac == null) {// No context instance was injected at construction time -> see if one// has been registered in the servlet context. If one exists, it is assumed// that the parent context (if any) has already been set and that the// user has performed any initialization such as setting the context idwac = findWebApplicationContext();}if (wac == null) {// No context instance is defined for this servlet -> create a local onewac = createWebApplicationContext(rootContext);}if (!this.refreshEventReceived) {// Either the context is not a ConfigurableApplicationContext with refresh// support or the context injected at construction time had already been// refreshed -> trigger initial onRefresh manually here.synchronized (this.onRefreshMonitor) {onRefresh(wac);}}if (this.publishContext) {// Publish the context as a servlet context attribute.String attrName = getServletContextAttributeName();getServletContext().setAttribute(attrName, wac);}return wac;
}

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

DispatcherServlet.properties中有配置默认的策略)

	/*** This implementation calls {@link #initStrategies}.*/@Overrideprotected 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) {
1		initMultipartResolver(context);
2		initLocaleResolver(context);
3		initThemeResolver(context);
4		initHandlerMappings(context);
5		initHandlerAdapters(context);
6		initHandlerExceptionResolvers(context);
7		initRequestToViewNameTranslator(context);
8		initViewResolvers(context);
9		initFlashMapManager(context);}

方法initMultipartResolver、initLocaleResolver、initThemeResolver、initRequestToViewNameTranslator、initFlashMapManager的处理方式几乎都一样(1.2.3.7.8,9),从应用文中取出指定的Bean, 如果没有, 就使用默认的.方法initHandlerMappings、initHandlerAdapters、initHandlerExceptionResolvers的处理方式几乎都一样(4,5,6)

  1. 初始化文件上传解析器MultipartResolver:从应用上下文中获取名称为multipartResolver的Bean,如果没有名为multipartResolver的Bean,则没有提供上传文件的解析器
  2. 初始化区域解析器LocaleResolver:从应用上下文中获取名称为localeResolver的Bean,如果没有这个Bean,则默认使用AcceptHeaderLocaleResolver作为区域解析器
  3. 初始化主题解析器ThemeResolver:从应用上下文中获取名称为themeResolver的Bean,如果没有这个Bean,则默认使用FixedThemeResolver作为主题解析器
  4. 初始化处理器映射器HandlerMappings:处理器映射器作用,1)通过处理器映射器找到对应的处理器适配器,将请求交给适配器处理;2)缓存每个请求地址URL对应的位置(Controller.xxx方法);如果在ApplicationContext发现有HandlerMappings,则从ApplicationContext中获取到所有的HandlerMappings,并进行排序;如果在ApplicationContext中没有发现有处理器映射器,则默认BeanNameUrlHandlerMapping作为处理器映射器
  5. 初始化处理器适配器HandlerAdapter:作用是通过调用具体的方法来处理具体的请求;如果在ApplicationContext发现有handlerAdapter,则从ApplicationContext中获取到所有的HandlerAdapter,并进行排序;如果在ApplicationContext中没有发现处理器适配器,则默SimpleControllerHandlerAdapter作为处理器适配器
  6. 初始化异常处理器解析器HandlerExceptionResolver:如果在ApplicationContext发现有handlerExceptionResolver,则从ApplicationContext中获取到所有的HandlerExceptionResolver,并进行排序;如果在ApplicationContext中没有发现异常处理器解析器,则不设置异常处理器
  7. 初始化RequestToViewNameTranslator:其作用是从Request中获取viewName,从ApplicationContext发现有viewNameTranslator的Bean,如果没有,则默认使用DefaultRequestToViewNameTranslator
  8. 初始化视图解析器ViewResolvers:先从ApplicationContext中获取名为viewResolver的Bean,如果没有,则默认InternalResourceViewResolver作为视图解析器
  9. 初始化FlashMapManager:其作用是用于检索和保存FlashMap(保存从一个URL重定向到另一个URL时的参数信息),从ApplicationContext发现有flashMapManager的Bean,如果没有,则默认使用DefaultFlashMapManager

处理请求

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

/*** Process the actual dispatching to the handler.* <p>The handler will be obtained by applying the servlet's HandlerMappings in order.* The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters* to find the first that supports the handler class.* <p>All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers* themselves to decide which methods are acceptable.* @param request current HTTP request* @param response current HTTP response* @throws Exception in case of any kind of processing failure*/
@SuppressWarnings("deprecation")
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);// Determine handler for the current request.mappedHandler = getHandler(processedRequest);if (mappedHandler == null) {noHandlerFound(processedRequest, response);return;}// Determine handler adapter for the current request.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;}}if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;}// Actually invoke the handler.mv = ha.handle(processedRequest, response, mappedHandler.getHandler());if (asyncManager.isConcurrentHandlingStarted()) {return;}applyDefaultViewName(processedRequest, mv);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 NestedServletException("Handler dispatch failed", err);}processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);}catch (Exception ex) {triggerAfterCompletion(processedRequest, response, mappedHandler, ex);}catch (Throwable err) {triggerAfterCompletion(processedRequest, response, mappedHandler,new NestedServletException("Handler processing failed", err));}finally {if (asyncManager.isConcurrentHandlingStarted()) {// Instead of postHandle and afterCompletionif (mappedHandler != null) {mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);}}else {// Clean up any resources used by a multipart request.if (multipartRequestParsed) {cleanupMultipart(processedRequest);}}}
}

HandlerAdapter 在 Spring MVC 中使用了适配器模式

适配器模式, 也叫包装器模式. 简单来说就是目标类不能直接使用, 通过一个新类进行包装一下, 适配调用方使用.把两个不兼容的接口通过一定的方式使之兼容.HandlerAdapter 主要用于支持不同类型的处理器(如 Controller、HttpRequestHandler 或者Servlet 等),让它们能够适配统一的请求处理流程。这样,Spring MVC 可以通过一个统一的接口来处理来自各种处理器的请求.

在开始执行 Controller 之前,会先调用 预处理方法 applyPreHandle,而applyPreHandle 方法的实现源码如下:

	/*** Apply preHandle methods of registered interceptors.* @return {@code true} if the execution chain should proceed with the* next interceptor or the handler itself. Else, DispatcherServlet assumes* that this interceptor has already dealt with the response itself.*/boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {for (int i = 0; i < this.interceptorList.size(); i++) {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 中会获取所有的拦截器 HandlerInterceptor , 并执行拦截器中的preHandle 方法,这样就会咱们前面定义的拦截器对应上了,

如果拦截器返回true, 整个发放就返回true, 继续执行后续逻辑处理如果拦截器返回fasle, 则中断后续操作

适配器模式

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

适配器模式角色

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

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

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

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


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


/**
* slf4j接口
*/
interface Slf4jApi{void log(String message);
}
/**
* log4j
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;}@Overridepublic 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的桥接器项目slf4jlog4j12中

统一数据返回格式

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

// 后端统一返回结果
@Data
public class Result<T> {private int status;private String errorMessage;private T data;
}
package com.bite.book.config;import com.bite.book.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 {@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@Overridepublic 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方法进行具体操作处理

存在问题

测试修改图书的接口

解决方法

package com.bite.book.config;import com.bite.book.model.Result;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
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 {private static ObjectMapper mapper = new ObjectMapper();@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {// 这里可以自定义返回结果if (body instanceof String) {try {return mapper.writeValueAsString(Result.success(body))} catch (JsonProcessingException e) {throw new RuntimeException(e);}}return Result.success(body);}
}

原因分析

SpringMVC默认会注册一些自带的HttpMessageConverter (从先后顺序排列分别为ByteArrayHttpMessageConverter ,StringHttpMessageConverter , SourceHttpMessageConverter ,SourceHttpMessageConverter , AllEncompassingFormHttpMessageConverter )

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {//...public RequestMappingHandlerAdapter() {this.messageConverters = new ArrayList<>(4);this.messageConverters.add(new ByteArrayHttpMessageConverter());this.messageConverters.add(new StringHttpMessageConverter());if (!shouldIgnoreXml) {try {this.messageConverters.add(new SourceHttpMessageConverter<>());}catch (Error err) {// Ignore when no TransformerFactory implementation is available}}this.messageConverters.add(new 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 可以使用.

    protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {Object body;Class valueType;Object targetType;if (value instanceof CharSequence) {body = value.toString();valueType = String.class;targetType = String.class;} else {body = value;valueType = this.getReturnValueType(value, returnType);targetType = GenericTypeResolver.resolveType(this.getGenericType(returnType), returnType.getContainingClass());}if (this.isResourceType(value, returnType)) {outputMessage.getHeaders().set("Accept-Ranges", "bytes");if (value != null && inputMessage.getHeaders().getFirst("Range") != null && outputMessage.getServletResponse().getStatus() == 200) {Resource resource = (Resource)value;try {List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value());body = HttpRange.toResourceRegions(httpRanges, resource);valueType = body.getClass();targetType = RESOURCE_REGION_LIST_TYPE;} catch (IllegalArgumentException var19) {outputMessage.getHeaders().set("Content-Range", "bytes */" + resource.contentLength());outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());}}}MediaType selectedMediaType = null;MediaType contentType = outputMessage.getHeaders().getContentType();boolean isContentTypePreset = contentType != null && contentType.isConcrete();if (isContentTypePreset) {if (this.logger.isDebugEnabled()) {this.logger.debug("Found 'Content-Type:" + contentType + "' in response");}selectedMediaType = contentType;} else {HttpServletRequest request = inputMessage.getServletRequest();List acceptableTypes;try {acceptableTypes = this.getAcceptableMediaTypes(request);} catch (HttpMediaTypeNotAcceptableException var20) {int series = outputMessage.getServletResponse().getStatus() / 100;if (body != null && series != 4 && series != 5) {throw var20;}if (this.logger.isDebugEnabled()) {this.logger.debug("Ignoring error response content (if any). " + var20);}return;}List<MediaType> producibleTypes = this.getProducibleMediaTypes(request, valueType, (Type)targetType);if (body != null && producibleTypes.isEmpty()) {throw new HttpMessageNotWritableException("No converter found for return value of type: " + valueType);}List<MediaType> mediaTypesToUse = new ArrayList();Iterator var15 = acceptableTypes.iterator();MediaType mediaType;while(var15.hasNext()) {mediaType = (MediaType)var15.next();Iterator var17 = producibleTypes.iterator();while(var17.hasNext()) {MediaType producibleType = (MediaType)var17.next();if (mediaType.isCompatibleWith(producibleType)) {mediaTypesToUse.add(this.getMostSpecificMediaType(mediaType, producibleType));}}}if (mediaTypesToUse.isEmpty()) {if (this.logger.isDebugEnabled()) {this.logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);}if (body != null) {throw new HttpMediaTypeNotAcceptableException(producibleTypes);}return;}MediaType.sortBySpecificityAndQuality(mediaTypesToUse);var15 = mediaTypesToUse.iterator();while(var15.hasNext()) {mediaType = (MediaType)var15.next();if (mediaType.isConcrete()) {selectedMediaType = mediaType;break;}if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;break;}}if (this.logger.isDebugEnabled()) {this.logger.debug("Using '" + selectedMediaType + "', given " + acceptableTypes + " and supported " + producibleTypes);}}HttpMessageConverter converter;GenericHttpMessageConverter genericConverter;label183: {if (selectedMediaType != null) {selectedMediaType = selectedMediaType.removeQualityValue();Iterator var23 = this.messageConverters.iterator();while(var23.hasNext()) {converter = (HttpMessageConverter)var23.next();genericConverter = converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter)converter : null;if (genericConverter != null) {if (((GenericHttpMessageConverter)converter).canWrite((Type)targetType, valueType, selectedMediaType)) {break label183;}} else if (converter.canWrite(valueType, selectedMediaType)) {break label183;}}}if (body != null) {Set<MediaType> producibleMediaTypes = (Set)inputMessage.getServletRequest().getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);if (!isContentTypePreset && CollectionUtils.isEmpty(producibleMediaTypes)) {throw new HttpMediaTypeNotAcceptableException(this.getSupportedMediaTypes(body.getClass()));}throw new HttpMessageNotWritableException("No converter for [" + valueType + "] with preset Content-Type '" + contentType + "'");}return;}body = this.getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, converter.getClass(), inputMessage, outputMessage);if (body != null) {LogFormatUtils.traceDebug(this.logger, (traceOn) -> {return "Writing [" + LogFormatUtils.formatValue(body, !traceOn) + "]";});this.addContentDispositionHeader(inputMessage, outputMessage);if (genericConverter != null) {genericConverter.write(body, (Type)targetType, selectedMediaType, outputMessage);} else {converter.write(body, selectedMediaType, outputMessage);}} else if (this.logger.isDebugEnabled()) {this.logger.debug("Nothing to write: null body");}}

在 ((HttpMessageConverter) converter).write(body, selectedMediaType,outputMessage) 的处理中, 调用父类的write方法由于StringHttpMessageConverter 重写了addDefaultHeaders方法, 所以会执行子类的方法。

然而子类StringHttpMessageConverter 的addDefaultHeaders方法定义接收参数为String, 此时t为Result类型, 所以出现类型不匹配"Result cannot be cast to java.lang.String"的异常

统一异常处理

统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice 表示控制器通知类, @ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法事件

package com.bite.book.config;import com.bite.book.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 ErrorHandle {//全局异常处理@ExceptionHandlerpublic Object handler(Exception e) {return Result.fail(e.getMessage());}
}

接口返回为数据时, 需要加@ResponseBody 注解

以上代码表示,如果代码出现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 {@ExceptionHandlerpublic Object handler(Exception e) {return Result.fail(e.getMessage());}@ExceptionHandlerpublic Object handler(NullPointerException e) {return Result.fail("发生NullPointerException:"+e.getMessage());}@ExceptionHandlerpublic Object handler(ArithmeticException e) {return Result.fail("发生ArithmeticException:"+e.getMessage());}
}

当异常不属于上面三个异常的时候,出现的异常离上面三个异常哪个最近,就报哪个异常,就近原则

@ControllerAdvice 源码分析

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

/** Copyright 2002-2019 the original author or authors.** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at**      https://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/package org.springframework.web.bind.annotation;import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Component;/*** Specialization of {@link Component @Component} for classes that declare* {@link ExceptionHandler @ExceptionHandler}, {@link InitBinder @InitBinder}, or* {@link ModelAttribute @ModelAttribute} methods to be shared across* multiple {@code @Controller} classes.** <p>Classes annotated with {@code @ControllerAdvice} can be declared explicitly* as Spring beans or auto-detected via classpath scanning. All such beans are* sorted based on {@link org.springframework.core.Ordered Ordered} semantics or* {@link org.springframework.core.annotation.Order @Order} /* {@link javax.annotation.Priority @Priority} declarations, with {@code Ordered}* semantics taking precedence over {@code @Order} / {@code @Priority} declarations.* {@code @ControllerAdvice} beans are then applied in that order at runtime.* Note, however, that {@code @ControllerAdvice} beans that implement* {@link org.springframework.core.PriorityOrdered PriorityOrdered} are <em>not</em>* given priority over {@code @ControllerAdvice} beans that implement {@code Ordered}.* In addition, {@code Ordered} is not honored for scoped {@code @ControllerAdvice}* beans &mdash; for example if such a bean has been configured as a request-scoped* or session-scoped bean.  For handling exceptions, an {@code @ExceptionHandler}* will be picked on the first advice with a matching exception handler method. For* model attributes and data binding initialization, {@code @ModelAttribute} and* {@code @InitBinder} methods will follow {@code @ControllerAdvice} order.** <p>Note: For {@code @ExceptionHandler} methods, a root exception match will be* preferred to just matching a cause of the current exception, among the handler* methods of a particular advice bean. However, a cause match on a higher-priority* advice will still be preferred over any match (whether root or cause level)* on a lower-priority advice bean. As a consequence, please declare your primary* root exception mappings on a prioritized advice bean with a corresponding order.** <p>By default, the methods in an {@code @ControllerAdvice} apply globally to* all controllers. Use selectors such as {@link #annotations},* {@link #basePackageClasses}, and {@link #basePackages} (or its alias* {@link #value}) to define a more narrow subset of targeted controllers.* If multiple selectors are declared, boolean {@code OR} logic is applied, meaning* selected controllers should match at least one selector. Note that selector checks* are performed at runtime, so adding many selectors may negatively impact* performance and add complexity.** @author Rossen Stoyanchev* @author Brian Clozel* @author Sam Brannen* @since 3.2* @see org.springframework.stereotype.Controller* @see RestControllerAdvice*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {/*** Alias for the {@link #basePackages} attribute.* <p>Allows for more concise annotation declarations &mdash; for example,* {@code @ControllerAdvice("org.my.pkg")} is equivalent to* {@code @ControllerAdvice(basePackages = "org.my.pkg")}.* @since 4.0* @see #basePackages*/@AliasFor("basePackages")String[] value() default {};/*** Array of base packages.* <p>Controllers that belong to those base packages or sub-packages thereof* will be included &mdash; for example,* {@code @ControllerAdvice(basePackages = "org.my.pkg")} or* {@code @ControllerAdvice(basePackages = {"org.my.pkg", "org.my.other.pkg"})}.* <p>{@link #value} is an alias for this attribute, simply allowing for* more concise use of the annotation.* <p>Also consider using {@link #basePackageClasses} as a type-safe* alternative to String-based package names.* @since 4.0*/@AliasFor("value")String[] basePackages() default {};/*** Type-safe alternative to {@link #basePackages} for specifying the packages* in which to select controllers to be advised by the {@code @ControllerAdvice}* annotated class.* <p>Consider creating a special no-op marker class or interface in each package* that serves no purpose other than being referenced by this attribute.* @since 4.0*/Class<?>[] basePackageClasses() default {};/*** Array of classes.* <p>Controllers that are assignable to at least one of the given types* will be advised by the {@code @ControllerAdvice} annotated class.* @since 4.0*/Class<?>[] assignableTypes() default {};/*** Array of annotation types.* <p>Controllers that are annotated with at least one of the supplied annotation* types will be advised by the {@code @ControllerAdvice} annotated class.* <p>Consider creating a custom composed annotation or use a predefined one,* like {@link RestController @RestController}.* @since 4.0*/Class<? extends Annotation>[] annotations() default {};}

从上述源码可以看出 @ControllerAdvice 派生于 @Component 组件, 这也就是为什么没有五大注解, ControllerAdvice 就生效的原因.下面我们看看Spring是怎么实现的, 还是从DispatcherServlet 的代码开始分析.DispatcherServlet 对象在创建时会初始化一系列的对象:

public class DispatcherServlet extends FrameworkServlet {//...@Overrideprotected 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 forControllerAdviceBean: " + 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 方法,比如返回数据前调用统一数据封装.


initHandlerExceptionResolvers(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 forControllerAdviceBean: " + adviceBean);}ExceptionHandlerMethodResolver resolver = newExceptionHandlerMethodResolver(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();//根据异常类型, 查找匹配的异常处理方法//比如NullPointerException会匹配两个异常处理方法://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(NullPointerException e) 声明的NullPointerException深度为0,//相对于handler(Exception e) 声明的Exception 深度 为2//所以 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;}}//...
}

相关文章:

SpringBoot | 拦截器 | 统一数据返回格式 | 统一异常处理 | 适配器模式

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

Oracle清除水位

– 清除水位 ALTER TABLE 数据库名.表名 ENABLE ROW MOVEMENT; ALTER TABLE 数据库名.表名 SHRINK SPACE CASCADE; ALTER TABLE 数据库名.表名 DISABLE ROW MOVEMENT; – 回收统计信息 BEGIN DBMS_STATS.GATHER_TABLE_STATS(OWNNAME > ‘数据库名’, TABNAME > ‘表名’…...

软件工程——期末复习(2)

Part1&#xff1a;软件工程基本概念 软件程序文档数据 在软件工程中&#xff0c;软件通常被定为程序、文档和数据的集合。程序是按事先设计的功能和性能要求编写的指令序列&#xff1b;程序是完成指定功能的一段特定语言代码。文档是描述程序操作和使用的文档&#xff0c;是与…...

RAID1技术是什么?它的发展和工作原理如何?

RIAD1是一种先进的数据存储与冗余技术&#xff0c;设计用于解决现代分布式系统中常见的数据安全、数据一致性和高可用性等问题。随着云计算和大规模分布式存储系统的兴起&#xff0c;如何保障数据在高效传输与存储过程中仍然能具备足够的安全性和可靠性&#xff0c;成为了各大企…...

【Apache Paimon】-- 8 -- flink 创建 paimon connecor 的两种方式

目录 1、使用 catalog 创建非临时表 2、使用 with 创建 temporary 表 3、对比 4、参考 1、使用 catalog 创建非临时表 CREATE CATALOG my_catalog WITH (type = paimon,warehouse = hdfs:///path/to/warehouse );USE CATALOG my_catalog; CREATE TABLE `<your-paimon-…...

js进阶-关于运算符++

一、运算符与表达式 运算符按参与的运算单元数量分为&#xff1a;一元运算符、二元运算符和三元运算符&#xff1b;表达式是运算单元和运算符一起构成的&#xff1b;每个表达式都有一个运算后的返回值。 二、关于运算符 1.概述 运算符分为两部分&#xff0c;第一部分是返回运…...

三维地图,智慧城市,商业智能BI,数据可视化大屏(Cesiumjs/UE)

绘图工具 三维地图&#xff1a;Cesiumjs 建模方式&#xff1a;激光点云建模、航拍倾斜摄影建模、GIS建模、BIM建模、手工建模 建模工具&#xff1a;C4D Blender GeoBuilding ArcGIS Cesiumjs <!DOCTYPE html> <html lang"en"> <head><meta …...

通过EPEL 仓库,在 CentOS 7 上安装 OpenResty

通过EPEL 仓库&#xff0c;在 CentOS 7 上安装 OpenResty 通过EPEL 仓库&#xff0c;在 CentOS 7 上安装 OpenResty步骤 1: 安装 EPEL 仓库步骤 2: 安装 OpenResty步骤 3: 启动 OpenResty步骤 4: 设置开机自启步骤 5: 验证安装说明 通过EPEL 仓库&#xff0c;在 CentOS 7 上安装…...

每日一题 LCR 054. 把二叉搜索树转换为累加树

LCR 054. 把二叉搜索树转换为累加树 使用后序遍历即可 class Solution { public:TreeNode* convertBST(TreeNode* root) {int temp 0;dfs(root,temp);return root;}void dfs(TreeNode* root,int &temp){if(!root){return ;}dfs(root->right,temp);temp root->val;…...

【贪心算法】贪心算法五

贪心算法五 1.跳跃游戏 II2.跳跃游戏3.加油站3.单调递增的数字 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励&#xff0c;我们一起努力吧!&#x1f603;&#x1f603; 1.跳跃游戏 II 题目链接&…...

vue2播放视频和预览文件的组件以及使用方法

##文件预览组件 按照组件 解决展示pdf的问题 npm install pdfh5 npm install canvas2.8.0 --ignore-scripts npm install --save dommatrix npm install --save web-streams-polyfill解决excel和docx预览的问题 npm install vue-office/docx vue-demi0.14.6 npm inst…...

记录一下,解决js内存溢出npm ERR! code ELIFECYCLEnpm ERR! errno 134 以及 errno 9009

项目是个老项目&#xff0c;依赖包也比较大&#xff0c;咱就按正常流程走一遍来详细解决这个问题&#xff0c;先看一下node版本&#xff0c;我用的是nvm管理的&#xff0c;详细可以看我的其他文章 友情提醒&#xff1a;如果项目比较老&#xff0c;包又大&#xff0c;又有一些需…...

【批处理脚本】更改Windows系统中的 hosts 解析文件

概述 作用 修改 Windows 系统中的 hosts 文件&#xff0c;可以实现 插入 或 删除 条目。该脚本允许用户以管理员权限执行&#xff0c;将特定的域名解析到指定的 IP 地址 应用场景 非常适用于需要频繁或批量修改 hosts 文件的场景&#xff1a; 屏蔽网站、域名重定向、DNS 污染防…...

AIGC 与艺术创作:变革与机遇

在当今数字化时代&#xff0c;人工智能生成内容&#xff08;AIGC&#xff09;正以惊人的速度重塑着艺术创作的格局&#xff0c;为艺术家们带来了令人振奋的新机遇。 一.AIGC 的崛起与艺术领域的变革 随着人工智能技术的不断进步&#xff0c;AIGC 逐渐在艺术领域崭露头角。它依…...

String IP和Int IP的互相转换

android中&#xff0c;wifiManager.connectionInfo.ipAddress 可以获取到wifi的ip地址&#xff0c;但这是一个int值&#xff0c;如何转换为常见的如192.168.1.129这种形式&#xff0c;以及这种形式如何转换回int值的形式。 这里ip分为4段&#xff0c;每一段的值都是0 ~ 255&am…...

【大数据学习 | 面经】yarn的资源申请和分配的单位-Container

在yarn中&#xff0c;资源的申请和分配是以container为单位进行的&#xff0c;而不是直接以application和task为单位。 每个提交到yarn上的应用程序&#xff08;application&#xff09;都有一个对应的ApplicationMaster&#xff08;AM&#xff09;。这个AM负责与ResourceMana…...

php基础:文件处理2

1.文件属性 当我们在程序中操作文件时&#xff0c;可能会使用到文件的一些常见属性&#xff0c;比如文件的大小、类型、修改时间、访问时间以及权限等等。PHP 中提供了非常全面的用来获取这些属性的内置函数&#xff0c;如下表所示。 2.目录操作 新建目录&#xff1a;mkdir(路…...

gradle下载慢解决方案2024 /12 /1android studio (Windows环境)

gradle下载慢解决方案2024 /12 /1 默认环境配置好了,环境配置和程序安装请出门右转 打开软件,点击右上角设置,找到如下设置页 选择本地安装并制定好你已经安装好的 gradle 应用保存即可 全局插件环境配置(新版本可以直接在设置中添加了) 找对应位置添加国内源并把前面的内置源…...

使用Java将PDF文件解析成Excel文件

安装pom依赖 <!-- 解析pdf--><dependency><groupId>org.apache.pdfbox</groupId><artifactId>pdfbox</artifactId><version>2.0.27</version> <!-- 请检查并使用最新版本 --></dependency>测试读取pdf文件…...

怎么区分直线模组中的导程和行程?

直线模组是一种直线传动装置&#xff0c;安装方便&#xff0c;精度高&#xff0c;其使用范围广。直线模组中的导程和行程是两个不同的概念&#xff0c;它们分别描述了直线模组的不同性能参数。 直线模组的行程指的是模组能够正常移动的最大距离&#xff0c;即滑块预期移动的有效…...

算法日记 42 day 图论

今天来看看广度优先搜索&#xff0c;并且写几个题。刷到这里我才想起来&#xff0c;当时第一次面试的时候问的就是这个题&#xff0c;当时大概知道一点思路&#xff0c;但不清楚是图论方面的&#xff0c;更别说写出来了。 广度优先搜索&#xff08;BFS&#xff09; 不同于深度…...

STM32 GPIO 8种工作模式的应用场景

目录 一、四种输入模式1、模拟输入&#xff1a;2、浮空输入&#xff1a;3、上拉输入&#xff1a;4、下拉输入&#xff1a; 二、四种输出模式1、推挽输出&#xff1a;2、开漏输出&#xff1a;3、复用推挽输出&#xff1a;4、复用开漏输出&#xff1a; 一、四种输入模式 1、模拟…...

JVM 类加载器有哪些?双亲委派机制的作用是什么?如何自定义类加载器?

类加载器分类 大家好&#xff0c;我是码哥&#xff0c;可以叫我靓仔&#xff0c;《Redis 高手心法》畅销书作者。 先回顾下&#xff0c;在 Java 中&#xff0c;类的初始化分为几个阶段: 加载、链接&#xff08;包括验证、准备和解析&#xff09;和 初始化。 而 类加载器&#x…...

揭秘:短视频矩阵源码功能开发分析!!!

一、短视频矩阵系统源码概述 短视频矩阵系统源码旨在为内容创作者及企业提供一种高效的工具&#xff0c;以实现对多个短视频账户的一站式管理。该系统支持同时管理、发布、监控和优化多达1000个短视频账户&#xff0c;显著提升了操作效率。 二、主要功能 1. 多账号管理 该系…...

leetcode - 2337. Move Pieces to Obtain a String

Description You are given two strings start and target, both of length n. Each string consists only of the characters ‘L’, ‘R’, and ‘_’ where: The characters ‘L’ and ‘R’ represent pieces, where a piece ‘L’ can move to the left only if there i…...

第九篇:k8s 通过helm发布应用

什么是helm&#xff1f; Helm 是 Kubernetes 的包管理器。Helm 是查找、分享和使用软件构建 Kubernetes 的最优方式。 在红帽系的Linux中我们使用yum来管理RPM包&#xff0c;类似的&#xff0c;在K8s中我们可以使用helm来管理资源对象&#xff08;Deployment、Service、Ingress…...

MySQL:锁机制

锁是计算机协调多个进程或线程并发访问某一资源的机制&#xff08;避免争抢&#xff09;。 在数据库中&#xff0c;除传统的计算资源&#xff08;如 CPU、RAM、I/O 等&#xff09;的争用以外&#xff0c;数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效…...

在lio_sam中融入GPS

文章目录 概要GPS里程计GPS因子反算后的GPS里程计概要 在LIO(激光惯性里程计)系统中,将GPS信息融合到里程计中,借助GTSAM(Georgia Tech Smoothing and Mapping)库进行因子图优化,可以有效提升全局定位精度。 GPS里程计 利用GeographicLib第三方库将经纬度投影到局部笛…...

快速构建NLP理论知识体系

NLP理论知识体系 一句话解释NLPNLP模型及原理简述1、Rag 一句话解释NLP 如果我们要实现机器翻译、情感分析、问答系统、文本摘要、聊天机器人、构造智能化的辅助文件填写模板&#xff0c;NLP可以通过现成的模型对输入的语音、文字、图片进行处理&#xff08;分词、标词性、去停…...

长期稳定境外号码解决方案:内地用户如何打电话、接收短信和验证码

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 三大方案对比 📒📝 免费且稳定的境外号码📝 长期稳定且符合本地政策📝 适合低频使用者⚓️ 相关链接 ⚓️📖 介绍 📖 许多用户在日常生活中需要拨打境外电话、接收短信或验证码,尤其是跨境电商从业者,更是对境外号…...

SpringCloud 与 SpringBoot版本对应关系,以及maven,jdk

目录 SpringCloud 与 SpringBoot各版本的对应关系 方式一 Learn 方式二 OverView SpringBoot与JDK、maven 容器等对应关系 SpringCloud 与 SpringBoot各版本的对应关系 SpringCloudSpringBootFinchley2.0.xFinchley.SR1Spring Boot >=2.0.3.RELEASE and <=2.0.9RELEAS…...

FSMC实验

FSMC &#xff0c;即灵活的静态存储控制器&#xff0c;能够与同步或异步存储器和 16 位 PC 存储器卡连接&#xff0c; STM32 的 FSMC 接口支持包括 SRAM 、 NAND FLASH 、 NOR FLASH 和 PSRAM 等存储器。 从上图我们可以看出&#xff0c; STM32 的 FSMC 将外部设…...

K8s命令大全(Complete List of K8s Commands)

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 本人主要分享计算机核心技…...

C++学习笔记

小甲鱼学习课程 02 #include <iostream> 头文件 iostream iostream 翻译为IO流 输入输出流 using namespace std; 引入命名空间 c标准库所使用的所有标识符都是在同一个特殊的名字空间std中来定义的&#xff1b; 偷懒使用这句话&#xff1b; std::cout <…...

伺服控制电机多大功率合适?

随着现代工业自动化的发展&#xff0c;伺服控制电机在各个行业的应用愈加广泛。伺服电机以其精确的控制、高效的运行和高响应速度&#xff0c;成为许多机械设备中不可或缺的组成部分。然而&#xff0c;在选择伺服电机时&#xff0c;确定其功率大小是一个关键环节。那么&#xf…...

《OpenCV 基础全攻略:从入门到精通》

《OpenCV 基础全攻略&#xff1a;从入门到精通》 一、OpenCV 简介&#xff08;一&#xff09;特点&#xff08;二&#xff09;优势&#xff08;三&#xff09;应用场景 二、安装与配置三、OpenCV 函数详解1. 图像读取函数 cv2.imread ()2. 图像显示函数 cv2.imshow ()3. 图像保…...

spring ai如何使用function call调用第三方模型

这里写自定义目录标题 背景什么是function call怎么用function call&#xff1f;总结 背景 一直困惑于ai是如何使用插件或者其他一些功能的&#xff0c;后来发现&#xff0c;很多大模型都支持function call功能&#xff0c;如何让大模型能够联网查询呢&#xff0c;function ca…...

2024 数学建模国一经验分享

2024 数学建模国一经验分享 背景&#xff1a;武汉某211&#xff0c;专业&#xff1a;计算机科学 心血来潮&#xff0c;就从学习和组队两个方面指点下后来者&#xff0c;帮新人避坑吧 2024年我在数学建模比赛中获得了国一&#xff08;教练说论文的分数是湖北省B组第一&#xff0…...

javaweb-Mybaits

1.Mybaits入门 &#xff08;1&#xff09;介绍 &#xff08;2&#xff09; 2.Mybaits VS JDBC 3.数据库连接池 &#xff08;1&#xff09;SpringBoot默认连接池为hikari&#xff0c;切换为Druid有两种方式 方式一&#xff1a;加依赖 方式二&#xff1a;直接修改配置文件 …...

108.【C语言】数据结构之二叉树查找值为x的节点

目录 1.题目 代码模板 2.分析 分类讨论各种情况 大概的框架 关键部分(继续递归)的详解 递归调用展开图 3.测试结果 其他写法 4.结论 5.注意事项 不推荐的写法 1.题目 查找值为x的节点并返回节点的地址 代码模板 typedef int BTDataType; typedef struct BinaryT…...

Ant-Design-Vue 全屏下拉日期框无法显示,能显示后小屏又位置错乱

问题1&#xff1a;在全屏后 日期选择器的下拉框无法显示。 解决&#xff1a;在Ant-Design-Vue的文档中&#xff0c;很多含下拉框的组件都有一个属性 getPopupContainer可以用来指定弹出层的挂载节点。 在该组件上加上 getPopupContainer 属性,给挂载到最外层盒子上。 <temp…...

sec啥意思

sec的基本含义是秒&#xff0c;是时间的基本单位之一&#xff1b;在数学中表示正割函数&#xff1b;在计算机科学中有时指安全&#xff1b;在法语中意为干的等。 sec的多重含义与应用 sec在数学中的定义 在数学领域&#xff0c;尤其是三角学中&#xff0c;sec代表正割函数&a…...

云计算vspere 安装过程

1 材料的准备 1 安装虚拟机 vmware workstation 2 安装esxi 主机 3 在esxi 主机上安装windows 2018 dns 服务器 4 在虚拟机上安装windows 2018 服务器 6 安装vcenter 5 登入界面测试 这里讲一下&#xff0c;由于部署vspere 需要在windows 2012 服务器上部…...

spring-boot打包前重新拉取maven依赖

在使用 Maven 构建 Spring Boot 项目时&#xff0c;如果希望在每次打包时都强制拉取依赖&#xff0c;可以通过以下方法实现。 方法一&#xff1a;在命令行强制更新依赖 在执行 mvn package 或 mvn install 等命令时&#xff0c;添加 -U 参数&#xff1a; mvn clean package -…...

华为云域名网站,域名切换到Cloudflare CDN出现访问报错:DNS 重定向次过多

网站域名切换到Cloudflare出现访问报错&#xff1a;重定向次过多&#xff0c;应该如何处理&#xff1f; 最近我自己已经遇到很多次这个情况了&#xff0c;将网站域名DNS切换到Cloudflare之后&#xff0c;网站会打不开&#xff0c;出现重定向次数过多报错。 网站域名切换到Clo…...

浔川AI翻译v5.1.0版本正式亮相!

浔川 AI 翻译 v5.1.0 版本正式亮相&#xff01; 在科技浪潮汹涌澎湃的今天&#xff0c;浔川 AI 翻译以其卓越的技术实力和对用户需求的精准把握&#xff0c;自豪地向全球用户宣布&#xff1a;浔川 AI 翻译 v5.1.0 版本正式闪耀登场&#xff01;这一全新版本在继承以往优势的基础…...

Merkle 树 应用在 代码工程

将 Merkle 树 应用在 代码工程 中&#xff0c;尤其是在大型项目中&#xff0c;可以帮助管理和验证代码的完整性、追踪代码变更、提高版本控制的效率等。通过将代码文件、类、函数等结构映射到 Merkle 树中&#xff0c;我们可以高效地验证代码库的任何变更&#xff0c;确保每个部…...

如何将表中存储的 JSON 数据转换为新表

如何将表中存储的 JSON 数据转换为新表 引言 本文介绍如何使用 PostgreSQL 的 JSON 函数&#xff0c;将 log_table 表中的 param_json 字段&#xff08;存储为 JSON 数组&#xff09;转换为一张新的表&#xff0c;并提取出具体的字段值。 表结构说明 假设 log_table 表的结…...

鸿蒙开发——使用ArkTs处理XML文本

1、概 述 XML&#xff08;可扩展标记语言&#xff09;是一种用于描述数据的标记语言&#xff0c;旨在提供一种通用的方式来传输和存储数据&#xff0c;特别是Web应用程序中经常使用的数据。XML并不预定义标记。因此&#xff0c;XML更加灵活&#xff0c;并且可以适用于广泛的应…...

借助vector实现进制转换详解

进制转换&#xff0c;没什么可说的&#xff0c;大一级别的水平&#xff0c;不过在某些考研题目中可能会涉及到顺序栈的实现&#xff0c;本贴不使用顺序栈&#xff0c;用STL里面的vector模拟一下&#xff1a;关键在于想清楚【除留取余】的逻辑&#xff0c;至于用什么结构存放中间…...