一、前言

大家好,我是 去哪里吃鱼 ,也叫小张。

最近从单位离职了,离开了五年多来朝朝夕夕皆灯火辉煌的某网,激情也好悲凉也罢,觥筹场上屡屡物是人非,调转过事业部以为能换种情绪,岂料和下了周五的班的前同事兼好朋友,匆匆赶往藏身巷弄的小菜馆里时,又次次被迫想起,那破晓时分大厦头顶有点吝啬的阳光。

阿坤:但凡拿我们当自己人,就不会这样...

我 :也许人家想好好表现呢

阿坤:算了,不说了,走着走着天要亮了,回去睡吧

我 :卧槽,真的是,行了不说了,趁着下面还没亮,赶紧回去睡吧

阿坤:下午见

小张目前蜗居赋闲,顺便养一下左肩。

想念七七。

扯远了,不说了,今天来给大家说一下 Spring Web 模块(基于 Servlet)中的异常(以下简称 Spring 异常)处理机制,看完文章,你应该会懂得在 web 开发过程中,怎么处理程序出现的异常。

本文基于 springboot 2.5.1 , 对应 spring framework 版本为 5.3.8

二、本文的异常种类划分

  1. "你妹啊,谁在 service 里面抛了个自定义异常给我 controller !" ———— 业务代码引起的异常
  2. "你这报文签名不对,参数也不对,拦截器都没过" ———— 拦截器异常
  3. "这是什么错误啊,status=500,啥也没有显示啊" ———— errorPath 异常

三、Spring Web 模块的请求核心流程解析

上述错误,都是用户在使用浏览器或者 APP 等访问后台时候出现的异常,因此我们有必要去了解一下 Spring Web 模块对用户请求的核心处理流程,只有当熟悉了请求处理流程,我们处理起异常来,才会得心应手。

3.1 那个 Servlet

每一个基于 Servlet 的 web 框架,都会有自己的 Servlet 实现,在 Spring Web 中,它叫 DispatcherServlet ,你所有的请求都会经过它来处理。

而在 Spring 的设计中,DispatcherServlet 中处理请求的那个方法,叫 doDispatch()

3.2 那个 doDispatch()

话不多说,先来看我精简过的方法

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
// 。。。小张替你省略部分代码。。。
try {
ModelAndView mv = null;
Exception dispatchException = null; try {
// 。。。小张替你省略部分代码。。。 // 根据 request 获取对应的 Handler
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
} // 根据 Handler 类型找到合适的 Handler 适配器,DispatcherServilet 通过适配器间接调用 Handler ,
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // 。。。小张替你省略部分代码。。。 // 重头戏来了
// 步骤1. 下面这一行会遍历拦截器,执行所有拦截器的 preHandle() 方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
} // 步骤2. 当所有拦截器都校验通过的时,下面这一行执行目标 controller 对应的业务方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); // 。。。小张替你省略部分代码。。。 // 步骤3. 当目标 controller 的业务方法执行完毕之后,下面这一行执行所有拦截器的 postHandler() 方法
mappedHandler.applyPostHandle(processedRequest, response, mv); // 步骤4. 下面有两个异常,捕获了 拦截器 preHandler 阶段和 controller 的业务方法执行阶段抛出的异常
} catch (Exception ex) {
dispatchException = ex;
} catch (Throwable err) {
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 步骤5. 如果步骤4有异常,就会在这里处理
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); // 步骤6. 下面两个异常调用所有拦截器的 fterCompletion方法
} catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
} catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,new NestedServletException("Handler processing failed", err));
}finally {
// 。。。小张替你省略部分代码。。。
}
}

上面代码中,最里面的那个 try ... catch 已经把常用情况下的 拦截器、controller 的异常捕获到了,异常处理逻辑在 步骤5 里面:

private void processDispatchResult(HttpServletRequest request,
HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler,
@Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
// 。。。小张替你省略部分代码。。。 if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
// 。。。小张替你省略部分代码。。。
} else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
// 调用 DispatcherServlet 异常处理流程
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// 。。。小张替你省略部分代码。。。
} ModelAndView processHandlerException(HttpServletRequest request,
HttpServletResponse response,
@Nullable Object handler,
Exception ex) throws Exception {
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
// 遍历 DispatcherServlet 里加载好的异常处理器
for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
// 交给异常处理器处理
exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (exMv != null) {
// 找到合适的异常处理器就中断
break;
}
}
}
// 。。。小张替你省略部分代码。。。
}

异常最终会交给 DispatcherServlet 里的 this.handlerExceptionResolvers 集合来处理,而这个东西也是我们自己规划的异常处理器最终汇聚的地方,它的类型是 HandlerExceptionResolver 接口

3.3 那个 HandlerExceptionResolver

这是一个接口,只有异常处理的方法签名

public interface HandlerExceptionResolver {

    @Nullable
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex); }

注意,返回值 ModelAndView 不为空,证明该异常处理器处理了异常,spring 不会再让剩下的异常处理器处理该异常

四、异常处理手段

章节二的划分对应的处理手段有下面这几种,我们一一举例

4.1 controller 中的业务代码引起的异常处理方式

4.1.1 简单点,使用 @ControllerAdvice 注解和 @ExceptionHandler 注解

// 步骤1. 使用注解修饰异常处理类
@ControllerAdvice
public class ErrorHandlerDemo { // 步骤2. 搭配使用注解,处理指定异常
@ExceptionHandler(CacheException.class)
@ResponseBody
public String cacheException(HandlerMethod handlerMethod, Exception e) {
return defaultErrorHandler(handlerMethod, e);
} // 步骤2. 搭配使用注解,处理指定异常
@ExceptionHandler(value = Exception.class)
@ResponseBody
public String defaultErrorHandler(HandlerMethod handlerMethod, Exception e) {
return revertMessage(e);
} private String revertMessage(Exception e) {
String msg = "系统异常";
if (e instanceof CacheException) {
msg = e.getMessage();
}
return msg;
}
}

对应异常处理的方法返回值类型,类比 @RequestMapping 方法的返回值类型,比如,也可以是 ModelAndView 类型

原理剖析

A. @ControllerAdvice + @ExceptionHandler 注解修饰的类的解析

首先,被 @ControllerAdvice 注解修饰的类,会被 Spring 包装成 ControllerAdviceBean ,这个东西把修饰的类的 Class<?> 保存成 beanType ,并且是 ExceptionHandlerMethodResolver 的构造函数入参,唯一的构造函数唯一的入参。

ExceptionHandlerMethodResolver 又是个什么东西? 异常处理器方法解析器?什么玩意儿!先看它都干了什么吧

public class ExceptionHandlerMethodResolver {

public static final MethodFilter EXCEPTION_HANDLER_METHODS = method -> AnnotatedElementUtils.hasAnnotation(method, ExceptionHandler.class);

    // 参数 handlerType 就是上面说提到的 beanType,就是上面实例代码中的 ErrorHandlerDemo 类
public ExceptionHandlerMethodResolver(Class<?> handlerType) { // 这个 EXCEPTION_HANDLER_METHODS 是个函数式接口
// MethodIntrospector.selectMethods() 方法用来查找 @ControllerAdvice 类里面被 @ExceptionHandler 注解修饰的方法并缓存起来
for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) { // 以 异常类型:异常处理方法 格式缓存起来
addExceptionMapping(exceptionType, method);
}
}
} }

细心读下注释,可以发现 ExceptionHandlerMethodResolver 已经把我们自定义的异常处理类和异常处理方法都已经收集、准备完毕了。

有人说了,我搞了多个 @ControllerAdvice 修饰的类啊,你敢不敢都给解析了?

敢!接下来就告诉你 @ControllerAdvice 修饰的类在哪里解析的!

B. @ControllerAdvice + @ExceptionHandler 注解修饰的类的加载

有个类叫 ExceptionHandlerExceptionResolver (什么玩意儿?异常处理器异常解析器?),别懵,不翻译它,看它是个啥

public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
implements ApplicationContextAware, InitializingBean {
@Nullable
private ApplicationContext applicationContext; // 没错,就是这里,缓存了 ErrorHandlerDemo 和它内部的异常处理方法对应的 ExceptionHandlerMethodResolver
private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache = new LinkedHashMap<>(); // 。。。小张替你省略部分代码。。。 // 这里是 InitializingBean 的实现方法
@Override
public void afterPropertiesSet() {
// 这就是加载 ControllerAdviceBean 的地方
initExceptionHandlerAdviceCache(); // 。。。小张替你省略部分代码。。。
} private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
} // 加载重点来了,通过上 Spring 上下文获取被 @ControllerAdvice 修饰的 ErrorHandlerDemo
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);
} // 创建与 ErrorHandlerDemo 中的异常处理方法对应的 ExceptionHandlerMethodResolver,就是上面A小节的解析部分
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType); if (resolver.hasExceptionMappings()) {
// ControllerAdviceBean 与 ExceptionHandlerMethodResolver 一一对应,缓存起来
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
} // 。。。小张替你省略部分代码。。。
} // 。。。小张替你省略部分代码。。。
}
}

那么到这里就明白了, ExceptionHandlerExceptionResolver 里面缓存了 ControllerAdivceBean 和它对应的具体的异常处理方法包装(即 ExceptionHandlerMethodResolver)。

读到这里,也许朋友你会问, ExceptionHandlerExceptionResolver 是缓存了,但是,这个玩意儿怎么用的呢,在哪里用的呢?

C. @ControllerAdvice + @ExceptionHandler 的启用

在 spring-webmvc 模块中,有个类叫 WebMvcConfigurationSupport 它用来支持 web 的相关配置,他有一个创建 Bean 的方法

public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
// 。。。小张替你省略部分代码。。。 /**
* 创建异常处理器组合
**/
@Bean
public HandlerExceptionResolver handlerExceptionResolver(@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
// 这里是空方法1,可以自定义实现,一般不拓展这个方法
configureHandlerExceptionResolvers(exceptionResolvers);
// 如果没有重写上面的方法,则会走这里,创建默认的异常处理器
if (exceptionResolvers.isEmpty()) {
addDefaultHandlerExceptionResolvers(exceptionResolvers, contentNegotiationManager);
}
// 这里是空方法2,可以自定义实现,一般拓展这个方法往所给的异常处理器集合里添加自定义异常处理器
extendHandlerExceptionResolvers(exceptionResolvers); // 把异常处理器集合组装到 HandlerExceptionResolverComposite 里, 而 HandlerExceptionResolverComposite 是接口 HandlerExceptionResolver 的实现类
HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
composite.setOrder(0);
composite.setExceptionResolvers(exceptionResolvers);
return composite;
} /**
* 根据提供的异常处理器创建异常处理器组合
**/
protected final void addDefaultHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers,
ContentNegotiationManager mvcContentNegotiationManager) {
//创建 B 章节里的异常处理器
ExceptionHandlerExceptionResolver exceptionHandlerResolver = createExceptionHandlerExceptionResolver();
// 。。。小张替你省略部分代码。。。 // 调用 InitializingBean 接口方法
exceptionHandlerResolver.afterPropertiesSet();
// 添加创建的异常处理器到集合中
exceptionResolvers.add(exceptionHandlerResolver); // 。。。小张替你省略部分代码。。。
} // 直接 new 了一个 ExceptionHandlerExceptionResolver
protected ExceptionHandlerExceptionResolver createExceptionHandlerExceptionResolver() {
return new ExceptionHandlerExceptionResolver();
}
}

上面的 @Bean 创建方法做了下面这些事

  1. 提供异常处理器自定义拓展方法 configureHandlerExceptionResolvers()extendHandlerExceptionResolvers()

  2. 如果没有指定异常处理器,只是拓展异常处理器,则创建默认异常处理器 ExceptionHandlerExceptionResolver

  3. 根据提供的异常处理器创建处理器组合对象 HandlerExceptionResolverComposite ,其也是异常处理器

到此为止 @Bean 方法已经做完了异常处理器的整合过程,常常与 @Bean 方法搭配使用的,是 @Configuration 注解修饰的配置类,然而 WebMvcConfigurationSupport 并没有这个注解

鸡贼的 spring 把 @Configuration 注解放到了它的子类 DelegatingWebMvcConfiguration 上!

package org.springframework.web.servlet.config.annotation;

// 配置类,自动加载
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport { private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite(); // 这个 WebMvcConfigurer 就是我们在 web 项目中自定义拦截器、异常处理器等需要实现的接口
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
}
}

到这里,使用 @ControllerAdvice 注解和 @ExceptionHandler 注解来处理 Controller 异常的整个加载流程已经剖析完毕了

4.1.2 自定义 HandlerExceptionResolver

[章节 3.3 ](## 3.3 HandlerExceptionResolver)(markdown 什么时候原生支持页面内跳转)已经有了接口简单描述,我们直接来个接口实现类 demo

public class ExceptionHandlerDemo implements HandlerExceptionResolver {

    private final ModelAndView EMPTY_MODEL_VIEW = new ModelAndView();

    @Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) {
// 自定义异常处理逻辑,可以输出状态到 response
// 记得返回一个空 ModelAndView,证明异常已经被处理
return EMPTY_MODEL_VIEW;
}
}

现在实现类有了, 我们把它加载到 spring 的 web 环境中去

// 配置类
@Configuration
public class WebConfigDemo implements WebMvcConfigurer { @Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new IpInterceptor()).addPathPatterns(
Arrays.asList(
"/index",
"/apply",
"/product/i",
"/product/d/*"
));
} // 就是这里,添加异常处理器
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new ExceptionHandlerDemo());
}
}

嗯,就是这样简单。

什么?你问我加载进去之后怎么生效的?

在 章节 4.1.1 中的 C 小节有提到类 DelegatingWebMvcConfiguration ,它的 setConfigurers(List configurers) 方法自动注入了咱们的 WebConfigDemo 配置类,并且,它重写了其父类 WebMvcConfigurationSupport 中的 extendHandlerExceptionResolvers()configureHandlerExceptionResolvers() 方法

@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport { // 当作是一个 配置类集合
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite(); // 注入 WebConfigDemo
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
} @Override
protected void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
this.configurers.configureHandlerExceptionResolvers(exceptionResolvers);
} // 加载 ExceptionHandlerDemo
@Override
protected void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
this.configurers.extendHandlerExceptionResolvers(exceptionResolvers);
} // 。。。小张替你省略部分代码。。。
}

这样在章节 4.1.1 中 C 小节, 执行 @Bean 方法 handlerExceptionResolver() 方法时候,空方法2 就指向了这里,进而加载到自定义的 ExceptionHandlerDemo

4.2 拦截器中抛出的异常

拦截器有3个方法签名

public interface HandlerInterceptor {

    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
} default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
} default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
} }

其中针对方法 afterCompletion() 抛出的异常,spring 只是简单打印了一个错误日志,并没有处理,也许 spring 认为,到这里,请求内容已经处理完了,所以不再把错误返回给调用方

void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
for (int i = this.interceptorIndex; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
try {
interceptor.afterCompletion(request, response, this.handler, ex);
} catch (Throwable ex2) {
// 仅打印错误日志
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
}
}
}

那么剩下的两个方法,其实章节 3.2 当中已经指明了,preHandle()postHandle 方法内出现的异常,与 controller 实体请求中的异常一起被处理了,所以章节 4.1 当中的异常处理方式,对于拦截器异常,同样生效。

4.3 errorPath 异常

介绍 errorPath 之前,先说一下 spring 对于未捕获到的异常的处理方式

对于未捕获到的异常,spring 会返回 500 http 状态码给调用方,并且转发请求到一个指定地址,这个地址默认值为 /error

在 spring boot 中的默认配置为 server.error.path=/error

以上,小张姑且称之为:错误转发机制 ,其实不仅仅是 500 状态,404 状态也会转发,你还能再找出些状态吗 ?

那么我们可以自已实现一个 /error controller 来处理异常吗?可以的,得益于 SpringBoot,我们可以借助另外一个叫 ErrorAttributes 的 bean 来获取异常信息

当请求出现异常时候,我们可以从 Request 当中读取出来

@Controller
public class ErrorHandleController implements ErrorController { private final ErrorAttributes errorAttributes; // 注入 SpringBoot 已经替我们创建好的 ErrorAttributes
public ErrorHandleController(ErrorAttributes errorAttributes) {
this.errorAttributes = errorAttributes;
} @RequestMapping(value = "/error", produces = "text/html")
@ResponseBody
public String errorPage(HttpServletRequest request) {
return this.getErrorAttributesMapString(request);
} @RequestMapping(value = "/error")
@ResponseBody
public String errorHandler(HttpServletRequest request) {
return this.getErrorAttributesMapString(request);
} private String getErrorAttributesMapString(HttpServletRequest request) {
ServletWebRequest webRequest = new ServletWebRequest(request);
// 利用 ErrorAttributes 读取 request 当中的异常,这里仅仅是简单地打印到页面上
return this.errorAttributes.getErrorAttributes(webRequest, ErrorAttributeOptions.defaults()).toString();
} }

五、结尾

本文并没有提供相应的 demo 演示,只是侧重于带领大家把 spring 的异常处理从头到尾过一遍,如果想实验,自己动手,结果会更快乐的。

有疑问的同学,欢迎评论区留言交流。

想念七七。

一文带你掌握Spring Web异常处理方式的更多相关文章

  1. 一文带你了解 Spring 5.0 WebFlux 应用场景

    一.什么是 Spring WebFlux 下图截自 Spring Boot 官方网站: 结合上图,在了解 Spring WebFlux 之前,我们先来对比说说什么是 Spring MVC,这更有益我们 ...

  2. 一文带你认识Spring事务

    前言 只有光头才能变强. 文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y Spring事务管理我相信大家都用得很多,但可能仅仅 ...

  3. 一文带你深入浅出Spring 事务原理

    Spring事务的基本原理 Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的.对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行: 获 ...

  4. 一文带你了解Spring核心接口Ordered的实现及应用

    前言 最近在看框架的时候,发现了这个接口,在此进行总结,希望能够给大家帮助,同时提升自己. order接口的大体介绍 Spring框架中有这个一个接口,名字叫Ordered,联想我们在数据库中应用的O ...

  5. 【项目实践】一文带你搞定Spring Security + JWT

    以项目驱动学习,以实践检验真知 前言 关于认证和授权,R之前已经写了两篇文章: [项目实践]在用安全框架前,我想先让你手撸一个登陆认证 [项目实践]一文带你搞定页面权限.按钮权限以及数据权限 在这两篇 ...

  6. JDBC、ORM、JPA、Spring Data JPA,傻傻分不清楚?一文带你厘清个中曲直,给你个选择SpringDataJPA的理由!

    序言 Spring Data JPA作为Spring Data中对于关系型数据库支持的一种框架技术,属于ORM的一种,通过得当的使用,可以大大简化开发过程中对于数据操作的复杂度. 本文档隶属于< ...

  7. 从源码入手,一文带你读懂Spring AOP面向切面编程

    之前<零基础带你看Spring源码--IOC控制反转>详细讲了Spring容器的初始化和加载的原理,后面<你真的完全了解Java动态代理吗?看这篇就够了>介绍了下JDK的动态代 ...

  8. 40 篇原创干货,带你进入 Spring Boot 殿堂!

    两个月前,松哥总结过一次已经完成的 Spring Boot 教程,当时感受到了小伙伴们巨大的热情. 两个月过去了,松哥的 Spring Boot 教程又更新了不少,为了方便小伙伴们查找,这里再给大家做 ...

  9. 分分钟带你玩转 Web Services【2】CXF

    在实践中一直在使用 JAX-WS 构建 WebService 服务,服务还是非常稳定.高效的. 但还是比较好奇其他的 WebService 开源框架,比如:CXF/Axis2/Spring WS等. ...

随机推荐

  1. HTML区块

    1.HTML 可以通过 <div> 和 <span>将元素组合起来. 2.HTML <div> 元素 HTML <div> 元素是块级元素,它可用于组合 ...

  2. Elasticsearch(es)介绍与安装

    ### RabbitMQ从入门到集群架构: https://zhuanlan.zhihu.com/p/375157411 可靠性高 ### Kafka从入门到精通: https://zhuanlan. ...

  3. flex布局的总结

    1.开启了flex布局的元素叫: flex container 2.里面的直接子元素叫:flex items(默认情况下,所有item都会在一行显示) 3.display属性由flex和inline- ...

  4. 题解 P1276 校门外的树(增强版)

    前言 本蒟蒻重学线段树,发现了这道题可以用线段树做. 虽然数据范围很小可以直接暴力,但由于在练习线段树所以打算用线段树写这道题. 本题解针对已经有线段树基础的巨佬,不懂线段树原理的话可以学习线段树后再 ...

  5. 字节开源RPC框架Kitex的日志库klog源码解读

    前言 这篇文章将着重于分析字节跳动开源的RPC框架Kitex的日志库klog的源码,通过对比Go原生日志库log的实现,探究其作出的改进. 为了平滑学习曲线,我写下了这篇分析Go原生log库的文章,希 ...

  6. 渗透开源工具之sqlmap安装配置环境变量教程

    由于计算机安全牵涉到很多方面,建议自己在服务器上搭建自己的靶场,如何搭建靶场请订阅并查看作者上期教程,这里作者先为大家推荐一个免费开源升级靶场:https://hack.zkaq.cn/   在封神台 ...

  7. CNN Training Loop Refactoring Simultaneous Hyperameter Testing

    上例中, 尝试两个不同的值 为此: alt+shift可以有多个光标,再jupyter notebook中. alt+d,alt+shift,ctrl+鼠标左键多点几个,都可以同时选择多个目标,并进行 ...

  8. 2021.05.29【NOIP提高B组】模拟 总结

    T1 题意:给你一个图,可以不花代价经过 \(K\) 条边,问从起点到终点的最短路 考试的想法:设 \(dis_{i,j}\) 表示从起点免费了 \(j\) 条边到 \(i\) 的最短路 然后直接跑 ...

  9. Mybatis架构原理(二)-二级缓存源码剖析

    Mybatis架构原理(二)-二级缓存源码剖析 二级缓存构建在一级缓存之上,在收到查询请求时,Mybatis首先会查询二级缓存,若二级缓存没有命中,再去查询一级缓存,一级缓存没有,在查询数据库; 二级 ...

  10. distroless 镜像介绍及 基于cbl-mariner的.NET distroless 镜像的容器

    1.概述 容器改变了我们看待技术基础设施的方式.这是我们运行应用程序方式的一次巨大飞跃.容器编排和云服务一起为我们提供了一种近乎无限规模的无缝扩展能力. 根据定义,容器应该包含 应用程序 及其 运行时 ...