Spring Web 过滤器使用常见错误(上)

news2025/2/27 20:50:06

我们都知道,过滤器是 Servlet 的重要标准之一,其在请求和响应的统一处理、访问日志记录、请求权限审核等方面都有着不可替代的作用。在 Spring 编程中,我们主要就是配合使用@ServletComponentScan 和 @WebFilter 这两个注解来构建过滤器。

说起来比较简单,好像只是标记下这两个注解就一劳永逸了。但是我们还是会遇到各式各样的问题,例如工作不起来、顺序不对、执行多次等等都是常见的问题。这些问题的出现大多都是使用简单致使我们掉以轻心,只要你加强意识,大概率就可以规避了。

那么接下来我们就来学习两个典型的案例,并通过分析,带你进一步理解过滤器执行的流程和原理

案例 1:@WebFilter 过滤器无法被自动注入

假设我们要基于 Spring Boot 去开发一个学籍管理系统。为了统计接口耗时,可以实现一个过滤器如下:

@WebFilter
@Slf4j
public class TimeCostFilter implements Filter {
    public TimeCostFilter(){
        System.out.println("construct");
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("开始计算接口耗时");
        long start = System.currentTimeMillis();
        chain.doFilter(request, response);
        long end = System.currentTimeMillis();
        long time = end - start;
        System.out.println("执行时间(ms):" + time);
    }
}

这个过滤器标记了 @WebFilter。所以在启动程序中,我们需要加上扫描注解即 @ServletComponentScan)让其生效,启动程序如下:

@SpringBootApplication
@ServletComponentScan
@Slf4j
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
        log.info("启动成功");
    }
}

然后,我们提供了一个 StudentController 接口来供学生注册:

@Controller
@Slf4j
public class StudentController {
   
    @PostMapping("/regStudent/{name}")
    @ResponseBody
    public String saveUser(String name) throws Exception {
        System.out.println("用户注册成功");
        return "success";
    }
}

上述程序完成后,你会发现一切按预期执行。但是假设有一天,我们可能需要把 TimeCostFilter 记录的统计数据输出到专业的度量系统(ElasticeSearch/InfluxDB 等)里面去,我们可能会添加这样一个 Service 类:

@Service
public class MetricsService {

    @Autowired
    public TimeCostFilter timeCostFilter;
    //省略其他非关键代码

}

完成后你会发现,Spring Boot 都无法启动了:

***************************

APPLICATION FAILED TO START

***************************

 Description: Field timeCostFilter in com.spring.puzzle.web.filter.example1.MetricsService required a bean of type 'com.spring.puzzle.web.filter.example1.TimeCostFilter' that could not be found.

 为什么会出现这样的问题?既然 TimeCostFilter 生效了,看起来也像一个普通的 Bean,为什么不能被自动注入?

案例解析

这次我们换个方式,我先告诉你结论,你可以暂停几分钟想想关键点。

本质上,过滤器被 @WebFilter 修饰后,TimeCostFilter 只会被包装为 FilterRegistrationBean,而 TimeCostFilter 自身,只会作为一个 InnerBean 被实例化,这意味着 TimeCostFilter 实例并不会作为 Bean 注册到 Spring 容器。

所以当我们想自动注入 TimeCostFilter 时,就会失败了。知道这个结论后,我们可以带着两个问题去理清一些关键的逻辑:

1.FilterRegistrationBean 是什么?它是如何被定义的?

2.TimeCostFilter 是怎么实例化,并和 FilterRegistrationBean 关联起来的?

我们先来看第一个问题:FilterRegistrationBean 是什么?它是如何定义的?

实际上,WebFilter 的全名是 javax.servlet.annotation.WebFilter,很明显,它并不属于 Spring,而是 Servlet 的规范。当 Spring Boot 项目中使用它时,Spring Boot 使用了 org.springframework.boot.web.servlet.FilterRegistrationBean 来包装 @WebFilter 标记的实例。从实现上来说,即 FilterRegistrationBean#Filter 属性就是 @WebFilter 标记的实例。这点我们可以从之前给出的截图中看出端倪。

另外,当我们定义一个 Filter 类时,我们可能想的是,我们会自动生成它的实例,然后以 Filter 的名称作为 Bean 的名字来指向它。但是调试下你会发现,在 Spring Boot 中,Bean 名字确实是对的,只是 Bean 实例其实是 FilterRegistrationBean。

那么这个 FilterRegistrationBean 最早是如何获取的呢?这还得追溯到 @WebFilter 这个注解是如何被处理的。在具体解析之前,我们先看下 @WebFilter 是如何工作起来的。使用 @WebFilter 时,Filter 被加载有两个条件:

  • 声明了 @WebFilter;
  • 在能被 @ServletComponentScan 扫到的路径之下。

这里我们直接检索对 @WebFilter 的使用,可以发现 WebFilterHandler 类使用了它,直接在 doHandle() 中加入断点,开始调试,执行调用栈如下:

从堆栈上,我们可以看出对 @WebFilter 的处理是在 Spring Boot 启动时,而处理的触发点是 ServletComponentRegisteringPostProcessor 这个类。它继承了 BeanFactoryPostProcessor 接口,实现对 @WebFilter、@WebListener、@WebServlet 的扫描和处理,其中对于 @WebFilter 的处理使用的就是上文中提到的 WebFilterHandler。这个逻辑可以参考下面的关键代码:

class ServletComponentRegisteringPostProcessor implements BeanFactoryPostProcessor, ApplicationContextAware {
   private static final List<ServletComponentHandler> HANDLERS;
   static {
      List<ServletComponentHandler> servletComponentHandlers = new ArrayList<>();
      servletComponentHandlers.add(new WebServletHandler());
      servletComponentHandlers.add(new WebFilterHandler());
      servletComponentHandlers.add(new WebListenerHandler());
      HANDLERS = Collections.unmodifiableList(servletComponentHandlers);
   }
   // 省略非关键代码
   @Override
   public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
      if (isRunningInEmbeddedWebServer()) {
         ClassPathScanningCandidateComponentProvider componentProvider = createComponentProvider();
         for (String packageToScan : this.packagesToScan) {
            scanPackage(componentProvider, packageToScan);
         }
      }
   }
   
  private void scanPackage(ClassPathScanningCandidateComponentProvider componentProvider, String packageToScan) {
     // 扫描注解
     for (BeanDefinition candidate : componentProvider.findCandidateComponents(packageToScan)) {
        if (candidate instanceof AnnotatedBeanDefinition) {
           // 使用 WebFilterHandler 等进行处理
           for (ServletComponentHandler handler : HANDLERS) {
              handler.handle(((AnnotatedBeanDefinition) candidate),
                    (BeanDefinitionRegistry) this.applicationContext);
           }
        }
     }
  }

最终,WebServletHandler 通过父类 ServletComponentHandler 的模版方法模式,处理了所有被 @WebFilter 注解的类,关键代码如下:

public void doHandle(Map<String, Object> attributes, AnnotatedBeanDefinition beanDefinition,
      BeanDefinitionRegistry registry) {
   BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(FilterRegistrationBean.class);
   builder.addPropertyValue("asyncSupported", attributes.get("asyncSupported"));
   builder.addPropertyValue("dispatcherTypes", extractDispatcherTypes(attributes));
   builder.addPropertyValue("filter", beanDefinition);
   //省略其他非关键代码
   builder.addPropertyValue("urlPatterns", extractUrlPatterns(attributes));
   registry.registerBeanDefinition(name, builder.getBeanDefinition());
}

从这里,我们第一次看到了 FilterRegistrationBean。通过调试上述代码的最后一行,可以看到,最终我们注册的 FilterRegistrationBean,其名字就是我们定义的 WebFilter 的名字:

后续这个 Bean 的具体创建过程,这里不再赘述

现在,我们接着看第二个问题:TimeCostFilter 何时被实例化?

此时,我们想要的 Bean 被“张冠李戴”成 FilterRegistrationBean,但是 TimeCostFilter 是何时实例化的呢?为什么它没有成为一个普通的 Bean?

关于这点,我们可以在 TimeCostFilter 的构造器中加个断点,然后使用调试的方式快速定位到它的初始化时机,这里我直接给出了调试截图:

在上述的关键调用栈中,结合源码,你可以找出一些关键信息:

1. Tomcat 等容器启动时,才会创建 FilterRegistrationBean;

2. FilterRegistrationBean 在被创建时(createBean)会创建 TimeCostFilter 来装配自身,TimeCostFilter 是通过 ResolveInnerBean 来创建的;

3. TimeCostFilter 实例最终是一种 InnerBean,我们可以通过下面的调试视图看到它的一些关键信息:

 通过上述分析,你可以看出最终 TimeCostFilter 实例是一种 InnerBean,所以自动注入不到也就非常合理了。

问题修正

找到了问题的根源,解决就变得简单了。

从上述的解析中,我们可以了解到,当使用 @WebFilter 修饰过滤器时,TimeCostFilter 类型的 Bean 并没有注册到 Spring 容器中,真正注册的是 FilterRegistrationBean。这里考虑到可能存在多个 Filter,所以我们可以这样修改下案例代码:

@Controller
@Slf4j
public class StudentController {
    @Autowired
    @Qualifier("com.spring.puzzle.filter.TimeCostFilter")
    ​FilterRegistrationBean timeCostFilter;
 
}

这里的关键点在于:

  • 注入的类型是 FilterRegistrationBean 类型,而不是 TimeCostFilter 类型;
  • 注入的名称是包含包名的长名称, 即 com.spring.puzzle.filter.TimeCostFilter(不能用 TimeCostFilter),以便于存在多个过滤器时进行精确匹配。

经过上述修改后,代码成功运行无任何报错,符合我们的预期。

案例 2:Filter 中不小心多次执行 doFilter()

在之前的案例中,我们主要都讨论了使用 @ServletComponentScan + @WebFilter 构建过滤器过程中的一些常见问题。

而在实际生产过程中,如果我们需要构建的过滤器是针对全局路径有效,且没有任何特殊需求(主要是指对 Servlet 3.0 的一些异步特性支持),那么你完全可以直接使用 Filter 接口(或者继承 Spring 对 Filter 接口的包装类 OncePerRequestFilter),并使用 @Component 将其包装为 Spring 中的普通 Bean,也是可以达到预期的需求。

不过不管你使用哪一种方式,你都可能会遇到一个共同的问题:业务代码重复执行多次

考虑到上一个案例用的是 @ServletComponentScan + @WebFilter,这里我们不妨再以 @Component + Filter 接口的实现方式来呈现下我们的案例,也好让你对 Filter 的使用能了解到更多。

首先,还是需要通过 Spring Boot 创建一个 Web 项目,不过已经不需要

@ServletComponentScan:

@SpringBootApplication()
public class LearningApplication {
    public static void main(String[] args) {
        SpringApplication.run(LearningApplication.class, args);
        System.out.println("启动成功");
    }
}

StudentController 保持功能不变,所以你可以直接参考之前的代码。另外我们定义一个 DemoFilter 用来模拟问题,这个 Filter 标记了 @Component 且实现了 Filter 接口,已经不同于我们上一个案例的方式:

@Component
public class DemoFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            //模拟异常
            System.out.println("Filter 处理中时发生异常");
            throw new RuntimeException();
        } catch (Exception e) {
            chain.doFilter(request, response);
        }
        chain.doFilter(request, response);
    }
}

全部代码实现完毕,执行后结果如下:

Filter 处理中时发生异常
......用户注册成功
......用户注册成功

这里我们可以看出,业务代码被执行了两次,这并不符合我们的预期。

我们本来的设计目标是希望 Filter 的业务执行不会影响到核心业务的执行,所以当抛出异常时,我们还是会调用 chain.doFilter。不过往往有时候,我们会忘记及时返回而误入其他的 chain.doFilter,最终导致我们的 Filter 执行多次。

而检查代码时,我们往往不能立马看出问题。所以说,这是一个典型的错误,虽然原因很简单吧。不过借着这个案例,我们可以分析下为什么会执行两次,以深入了解 Filter 的执行。

案例解析

在解析之前,我先给你讲下 Filter 背后的机制,即责任链模式。

以 Tomcat 为例,我们先来看下它的 Filter 实现中最重要的类 ApplicationFilterChain。它采用的是责任(职责)链设计模式,在形式上很像一种递归调用。

但区别在于递归调用是同一个对象把子任务交给同一个方法本身去完成,而职责链则是一个对象把子任务交给其他对象的同名方法去完成。其核心在于上下文 FilterChain 在不同对象 Filter 间的传递与状态的改变,通过这种链式串联,我们就可以对同一种对象资源实现不同业务场景的处理,达到业务解耦。整个 FilterChain 的结构就像这张图一样:

这里我们不妨还是带着两个问题去理解 FilterChain:

1.FilterChain 在何处被创建,又是在何处进行初始化调用,从而激活责任链开始链式调用?

2.FilterChain 为什么能够被链式调用,其内在的调用细节是什么?

接下来我们直接查看负责请求处理的 StandardWrapperValve#invoke(),快速解决第一个问题:

public final void invoke(Request request, Response response)
    throws IOException, ServletException {
    // 省略非关键代码
    // 创建filterChain 
    ApplicationFilterChain filterChain =
        ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
// 省略非关键代码 
try {
    if ((servlet != null) && (filterChain != null)) {
        // Swallow output if needed
        if (context.getSwallowOutput()) {
             // 省略非关键代码 
             //执行filterChain
             filterChain.doFilter(request.getRequest(),
                            response.getResponse());
             // 省略非关键代码 
         }
// 省略非关键代码
}

通过代码可以看出,Spring 通过 ApplicationFilterFactory.createFilterChain() 创建 FilterChain,然后调用其 doFilter() 执行责任链。而这些步骤的起始点正是 StandardWrapperValve#invoke()。

接下来,我们来一起研究第二个问题,即 FilterChain 能够被链式调用的原因和内部细节。

首先查看 ApplicationFilterFactory.createFilterChain(),来看下 FilterChain 如何被创建,如下所示:

public static ApplicationFilterChain createFilterChain(ServletRequest request,
        Wrapper wrapper, Servlet servlet) {
    // 省略非关键代码
    ApplicationFilterChain filterChain = null;
    if (request instanceof Request) {
        // 省略非关键代码
        // 创建Chain 
        filterChain = new ApplicationFilterChain();
        // 省略非关键代码
    }
    // 省略非关键代码
    // Add the relevant path-mapped filters to this filter chain
    for (int i = 0; i < filterMaps.length; i++) {
        // 省略非关键代码
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
            context.findFilterConfig(filterMaps[i].getFilterName());
        if (filterConfig == null) {
            continue;
        }
        // 增加filterConfig到Chain
        filterChain.addFilter(filterConfig);
    }

    // 省略非关键代码
    return filterChain;
}

它创建 FilterChain,并将所有 Filter 逐一添加到 FilterChain 中。然后我们继续查看 ApplicationFilterChain 类及其 addFilter():

// 省略非关键代码
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
private int pos = 0;
private int n = 0;
// 省略非关键代码
void addFilter(ApplicationFilterConfig filterConfig) {
    for(ApplicationFilterConfig filter:filters)
        if(filter==filterConfig)
            return;

    if (n == filters.length) {
        ApplicationFilterConfig[] newFilters =
            new ApplicationFilterConfig[n + INCREMENT];
        System.arraycopy(filters, 0, newFilters, 0, n);
        filters = newFilters;
    }
    filters[n++] = filterConfig;
}

在 ApplicationFilterChain 里,声明了 3 个变量,类型为 ApplicationFilterConfig 的数组 Filters、过滤器总数计数器 n,以及标识运行过程中被执行过的过滤器个数 pos。

每个被初始化的 Filter 都会通过 filterChain.addFilter(),加入到类型为 ApplicationFilterConfig 的类成员数组 Filters 中,并同时更新 Filter 总数计数器 n,使其等于 Filters 数组的长度。到这,Spring 就完成了 FilterChain 的创建准备工作。

接下来,我们继续看 FilterChain 的执行细节,即 ApplicationFilterChain 的 doFilter():

public void doFilter(ServletRequest request, ServletResponse response)
    throws IOException, ServletException {
    if( Globals.IS_SECURITY_ENABLED ) {
        //省略非关键代码
        internalDoFilter(request,response);
        //省略非关键代码
    } else {
        internalDoFilter(request,response);
    }
}

这里逻辑被委派到了当前类的私有方法 internalDoFilter,具体实现如下:

private void internalDoFilter(ServletRequest request,
                              ServletResponse response){
    if (pos < n) {
        // pos会递增
        ApplicationFilterConfig filterConfig = filters[pos++];
        try {
            Filter filter = filterConfig.getFilter();
            // 省略非关键代码
            // 执行filter
            filter.doFilter(request, response, this);
            // 省略非关键代码
        } 
        // 省略非关键代码
        return;
    }
        // 执行真正实际业务
        servlet.service(request, response);
    } 
    // 省略非关键代码
}

我们可以归纳下核心知识点:

  • ApplicationFilterChain 的 internalDoFilter() 是过滤器逻辑的核心;
  • ApplicationFilterChain 的成员变量 Filters 维护了所有用户定义的过滤器;
  • ApplicationFilterChain 的类成员变量 n 为过滤器总数,变量 pos 是运行过程中已经执行的过滤器个数;
  • internalDoFilter() 每被调用一次,pos 变量值自增 1,即从类成员变量 Filters 中取下一个 Filter;
  • filter.doFilter(request, response, this) 会调用过滤器实现的 doFilter(),注意第三个参数值为 this,即为当前 ApplicationFilterChain 实例 ,这意味着:用户需要在过滤器中显式调用一次 javax.servlet.FilterChain#doFilter,才能完成整个链路;
  • pos < n 意味着执行完所有的过滤器,才能通过 servlet.service(request, response) 去执行真正的业务。

执行完所有的过滤器后,代码调用了 servlet.service(request, response) 方法。从下面这张调用栈的截图中,可以看到,经历了一个很长的看似循环的调用栈,我们终于从 internalDoFilter() 执行到了 Controller 层的 saveUser()。这个过程就不再一一细讲了。

分析了这么多,最后我们再来思考一下这个问题案例。

DemoFilter 代码中的 doFilter() 在捕获异常的部分执行了一次,随后在 try 外面又执行了一次,因而当抛出异常的时候,doFilter() 明显会被执行两次,相对应的 servlet.service(request, response) 方法以及对应的 Controller 处理方法也被执行了两次。

你不妨回过头再次查看上文中的过滤器执行流程图,相信你会有更多的收获。

问题修正

现在就剩下解决这个问题了。其实只需要删掉重复的 filterChain.doFilter(request, response) 就可以了,于是代码就变成了这样:

@Component
public class DemoFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            //模拟异常
            System.out.println("Filter 处理中时发生异常");
            throw new RuntimeException();
        } catch (Exception e) {
            //去掉下面这行调用
            //chain.doFilter(request, response);
        }
        chain.doFilter(request, response);
    }
}

 重新运行程序和测试,结果符合预期,业务只执行了一次。回顾这个问题,我想你应该有所警示:在使用过滤器的时候,一定要注意,不管怎么调用,不能多次调用 FilterChain#doFilter()。

重点回顾

通过这节课的学习,相信你对过滤器已经有了一个较为深入的了解,这里我们不妨再次梳理下关键知识点:

1.@WebFilter 这种方式构建的 Filter 是无法直接根据过滤器定义类型来自动注入的,因为这种 Filter 本身是以内部 Bean 来呈现的,它最终是通过 FilterRegistrationBean 来呈现给 Spring 的。所以我们可以通过自动注入 FilterRegistrationBean 类型来完成装配工作,示例如下:

    @Autowired
    @Qualifier("com.spring.puzzle.filter.TimeCostFilter")
    ​FilterRegistrationBean timeCostFilter;

2.我们在过滤器的执行中,一定要注意避免不要多次调用 doFilter(),否则可能会出现业务代码执行多次的问题。这个问题出现的根源往往在于“不小心”,但是要理解这个问题呈现的现象,就必须对过滤器的流程有所了解。可以看过滤器执行的核心流程图:

结合这个流程图,我们还可以进一步细化出以下关键步骤:

  • 当一个请求来临时,会执行到 StandardWrapperValve 的 invoke(),这个方法会创建 ApplicationFilterChain,并通过 ApplicationFilterChain#doFilter() 触发过滤器执行;
  • ApplicationFilterChain 的 doFilter() 会执行其私有方法 internalDoFilter;
  • 在 internalDoFilter 方法中获取下一个 Filter,并使用 request、response、this(当前 ApplicationFilterChain 实例)作为参数来调用 doFilter():
  • 在 Filter 类的 doFilter() 中,执行 Filter 定义的动作并继续传递,获取第三个参数 ApplicationFilterChain,并执行其 doFilter();
  • 此时会循环执行进入第 2 步、第 3 步、第 4 步,直到第 3 步中所有的 Filter 类都被执行完毕为止;
  • 所有的 Filter 过滤器都被执行完毕后,会执行 servlet.service(request, response) 方法,最终调用对应的 Controller 层方法 。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1477182.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

企业计算机服务器中了_locked勒索病毒怎么办,_locked勒索病毒解密数据恢复

在企业的生产运营工作中&#xff0c;网络的力量非常强大&#xff0c;可以为企业带来极大的便利性&#xff0c;越来越多的企业通过网络的力量开展各项工作&#xff0c;扩大业务范围&#xff0c;但在工作过程中&#xff0c;人们也需要警惕网络威胁的存在。近期&#xff0c;云天数…

向日葵、Todesk、teamviewer等工具远程连接电脑时第三方应用显示白屏

问题描述&#xff1a;用向日葵远程等桌面时&#xff0c;当把显示器断电或者就没有显示器时或者笔记本盖子合住时&#xff0c;第三方软件显示白屏或显示不出来的问题。 原因&#xff1a;某些显卡在断开屏幕时自动降为低功耗模式。 解决 1、下载工具 https://www.amyuni.com/d…

Vue.js入门指南:简介、环境配置与Yarn创建项目

一、Vue.js简介 Vue.js&#xff0c;一个流行的JavaScript框架&#xff0c;以其直观、灵活和高效的特点&#xff0c;在前端开发者中赢得了广泛的赞誉。Vue.js的核心库专注于视图层&#xff0c;使得开发者能够构建出响应式的数据绑定和组合的视图组件。Vue.js的目标是通过尽可能简…

IntelliJ IDEA 常用快捷键和下载链接

下载链接&#xff08;windows&#xff09; 下载 IntelliJ IDEA – 领先的 Java 和 Kotlin IDE 编码时&#xff1a; 跳转到引用方法的地方 &#xff08;有多个引用时会出现下拉列表&#xff09; ctrl鼠标左键 跳转后回到原来的地方 …

JVM运行时数据区——运行时数据区及线程概述

文章目录 1、运行时数据区概述2、线程3、小结 内存是非常重要的系统资源&#xff0c;是硬盘和CPU的中间仓库及桥梁&#xff0c;承载着操作系统和应用程序的实时运行。JVM在程序执行期间把它所管理的内存分为若干个不同的数据区域。这些不同的数据区域可以分为两种类型&#xff…

接口测试报错“REQUEST JSON PARSING FAILED”

经验分享&#xff1a;json文本通过微信发送会自动添加空格&#xff0c;复制粘贴的之后需要注意&#xff0c;先把空格去掉再请求。 1.问题现象&#xff1a;接口测试的时候请求响应提示json格式错误 2.问题排查&#xff1a;JSON在线解析及格式化验证 - JSON.cn 利用第三方json解…

Flutter Dio进阶:使用Flutter Dio拦截器实现高效的API请求管理和身份验证刷新

Flutter笔记 使用Flutter Dio拦截器实现高效的API请求管理和身份验证刷新 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article…

pclpy 拉格朗日乘子法拟合平面

pclpy 拉格朗日乘子法拟合平面 一、算法原理1.算法步骤 二、代码三、结果1.左边原点云数据&#xff0c;右边将点云拉格朗日乘子法拟合平面投影在该平面 四、相关数据 一、算法原理 1.算法步骤 对k一近邻点拟合平面&#xff0c;最小二乘法&#xff08;平面过重心&#xff09;&…

金三银四面试必问:Redis真的是单线程吗?

文章目录 01 Redis中的多线程1&#xff09;redis-server&#xff1a;2&#xff09;jemalloc_bg_thd3&#xff09;bio_xxx&#xff1a; 02 I/O多线程03 Redis中的多进程04 结论▼延伸阅读 由面试题“Redis是否为单线程”引发的思考 作者&#xff1a;李乐 来源&#xff1a;IT阅读…

【MySQL】数据管理——DML操作数据

目录 DML&#xff08;数据操作语言&#xff09;添加数据插入单行语法插入多行语法SQL示例将查询结果插入到新表中语法1&#xff1a;语法2&#xff1a; 修改数据语法示例关于SQL的运算符算术运算符比较运算符逻辑运算符 案例 删除数据DELETE命令语法 TRUNCATE TABLE 命令语法代码…

数据卷(Data Volumes)自定义镜像(Dockerfile)

目录 一.数据卷(Data Volumes) 二.自定义镜像(Dockerfile) 自定义centos 一.数据卷(Data Volumes) 数据卷(Data Volumes)是一个可供一个或多个容器使用的特殊目录&#xff0c;它将主机操作系统目录直接映射进容器&#xff0c;它可以提供很多有用的特性&#xff1a; 1.数据卷…

[DEBUG] spring boot-如何处理链接中的空格等特殊字符

问题&#xff1a; get或者post中提交的内容可能有空格、#等特殊字符&#xff0c;不做处理的话可能解析错误。 解决&#xff1a; html中&#xff1a; <a th:href"{/listSgrna(id${item.getGeneId()},geneName${item.getGeneName()},genome${genome},sgrnaNum${sgrnaN…

Java开发的核心模式 - MVC

文章目录 1、MVC设计模式2、Web开发本质3、服务器的性能瓶颈 1、MVC设计模式 MVC设计模式示意图 在整个Java学习之旅中&#xff0c;MVC&#xff08;Model-View-Controller&#xff09;设计模式无疑占据着极其重要的地位&#xff0c;堪称理解和掌握Java项目开发精髓的钥匙。如…

2024最新Android面试题目,【设计思想解读开源框架】

前言 从18年毕业至今&#xff0c;就职过两家公司&#xff0c;大大小小项目做了几个&#xff0c;非常感谢我的两位老大&#xff0c;在我的android成长路上给予我很多指导&#xff0c;亦师亦友的关系。 从年前至今参加面试了很多公司&#xff0c;也收到了几家巨头的offer&#…

【React架构 - Scheduler中的MessageChannel】

前序 我们都知道JS代码是在浏览器5个进程(下面有介绍)中渲染进程中的Js引擎线程执行的&#xff0c;其他还有GUI渲染线程、定时器线程等&#xff0c;而页面的布局和绘制是在GUI线程中完成的&#xff0c;这些线程之间是互斥的&#xff0c;所以在执行Js的同时会阻塞页面的渲染绘制…

Python实现向量自回归模型(VAR算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 向量自回归模型&#xff08;Vector Autoregression&#xff0c;简称VAR&#xff09;是一种多元时间序列…

国内 永远不会倒闭的四个行业

据教育部公布的数据&#xff0c;2024届高校毕业生人数预计将达到1187万人&#xff0c;较2023年增加29万&#xff0c;再创历史新高。 同时&#xff0c;受全球经济形势的影响&#xff0c;一些行业和领域的就业形势并不乐观&#xff0c;比如房地产、传统零售业、传统金融业、低技能…

Python 从文件中读取JSON 数据并解析转存

文章目录 文章开篇Json简介Json数据类型Json硬性规则Json数据转化网站Json和Dict类型转换json模块的使用Python数据和Json数据的类型映射json.dumps1.字典数据中含有**存在中文**2.json数据通过缩进符**美观输出**3.对Python数据类型中键进行**排序输出**4.json数据**分隔符的控…

Rocky Linux 运维工具 tar

一、tar的简介 tar​命令是Linux操作系统中用于打包和解压文件的工具。通过 ​tar​可以将多个文件或目录打包成一个归档文件&#xff0c;也可以解压这些归档文件。 二、tar的参数说明 -c 用于将指定的文件或目录打包成一个归档文件&#xff0c;即压缩归档文件 -f 用于指定归档…

Kubernetes activemq系列| k8s 部署activemq artemis

一、ActiveMQ Artemis介绍 Apache ActiveMQ Artemis 是一个高性能的开源消息代理,它完全符合 Java Message Service (JMS) 2.0 规范,并支持多种通信协议,包括 AMQP、MQTT、STOMP 和 OpenWire 等。ActiveMQ Artemis 由 Apache Software Foundation 开发和维护,旨在提供可靠…