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

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

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

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

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

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

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

@WebFilter
@Slf4j
public class TimeCostFilter implements Filter {public TimeCostFilter(){System.out.println("construct");}@Overridepublic 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}")@ResponseBodypublic String saveUser(String name) throws Exception {System.out.println("用户注册成功");return "success";}
}

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

@Service
public class MetricsService {@Autowiredpublic 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);}// 省略非关键代码@Overridepublic 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 neededif (context.getSwallowOutput()) {// 省略非关键代码 //执行filterChainfilterChain.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 chainfor (int i = 0; i < filterMaps.length; i++) {// 省略非关键代码ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMaps[i].getFilterName());if (filterConfig == null) {continue;}// 增加filterConfig到ChainfilterChain.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();// 省略非关键代码// 执行filterfilter.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 层方法 。

相关文章:

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

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

【数据结构】周末作业

1.new(struct list_head*)malloc(sizeof(struct list_head*)); if(newNULL) { printf("失败\n"); return; } new->nextprev->next; prev->nextnew; return; 2.struct list_head* pprev->next; prev->nextp->next; p->next->prevpr…...

java 企业培训管理系统Myeclipse开发mysql数据库web结构jsp编程计算机网页项目

一、源码特点 java 企业培训管理系统是一套完善的java web信息管理系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为Mysql5.0&…...

在SAP HANA中使用OData(二)

通常有两种方式通过OData来暴露SAP HANA中的数据库对象&#xff0c;一是直接使用Database Object&#xff0c;比如前一篇和本篇文章介绍的例子&#xff0c;这种方式针对于数据已经存在于SAP HANA中&#xff0c;在Repository中没有对应的设计时对象(Design-time Object)&#xf…...

【Docker】前端基于dockerfiel构建镜像部署,实现在容器启动时传递环境变量, 请求不同服务地址

前端部署采用 docker 的方式&#xff0c; 实现在容器启动时传递环境变量&#xff0c; 请求不同服务地址 实现思路&#xff1a; 定义.env.xxx 文件&#xff08;环境变量赋值&#xff09;&#xff0c;在compose.yml中引入.env.xxx 文件&#xff0c;环境变量通过nginx的sub_filte…...

评估测试接口软件与网站的使用方法及优劣势比较

评估测试接口软件与网站的使用方法及优劣势比较 导言 在软件开发和测试过程中&#xff0c;对接口进行测试是至关重要的一步。测试接口的软件和网站提供了各种工具和方法&#xff0c;以便开发人员和测试人员能够有效地测试他们的应用程序接口。本文将探讨几种常见的测试接口软…...

【Qt学习】QLineEdit 控件 属性与实例(登录界面,验证密码,正则表达式)

文章目录 1. 介绍2. 实例使用2.1 登录界面2.2 对比两次密码是否相同2.3 通过按钮显示当前输入的密码&#xff08;并对2.2进行优化&#xff09;2.4 结语 3. 正则表达式3.1 QRegExp3.2 验证输入内容 4. 资源代码 1. 介绍 关于 QLineEdit 的详细介绍&#xff0c;可以去查阅官方文…...

Spring Boot 和 Spring Cloud: 区别与联系

Spring Boot 和 Spring Cloud: 区别与联系 在当今软件开发领域&#xff0c;微服务架构和快速开发成为了主流趋势。Spring框架作为Java生态系统中最流行的开发框架之一&#xff0c;也不例外地推出了Spring Boot和Spring Cloud这两个项目来满足这些需求。本文将详细探讨它们之间…...

9.网络游戏逆向分析与漏洞攻防-游戏网络架构逆向分析-接管游戏连接服务器的操作

内容参考于&#xff1a;易道云信息技术研究院VIP课 上一个内容&#xff1a;游戏底层功能对接类GameProc的实现 码云地址&#xff08;master 分支&#xff09;&#xff1a;https://gitee.com/dye_your_fingers/titan 码云版本号&#xff1a;44c54d30370d3621c1e9ec3d7fa1e2a0…...

vue - - - - - vue3使用draggable拖拽组件

vue3使用draggable拖拽组件 一、组件安装二、插件使用三、遇到的问题1. missing required prop&#xff1a; “itemKey” 一、组件安装 yarn add vuedraggablenext // or npm i -S vuedraggablenext二、插件使用 <template><draggableitem-key"id"class&q…...

PHP语言常见面试题:请解释一下PHP是什么,以及它的主要用途是什么?

PHP&#xff0c;英文全称为Hypertext Preprocessor&#xff0c;中文名称为“超文本预处理器”。它是一种通用的开源脚本语言&#xff0c;特别适用于Web开发领域。PHP最初是由Rasmus Lerdorf在1995年创建的&#xff0c;并且自那时以来&#xff0c;它已经发展成为一个功能强大且易…...

Unity(第六部)向量的理解和算法

标量:只有大小的量。185 888 999 &#xff08;类似坐标&#xff09; 向量:既有大小&#xff0c;也有方向。&#xff08;类似以个体为主体的方向&#xff0c;前方一百米&#xff09; 向量的模:向量的大小。&#xff08;类似以个体为主体的方向&#xff0c;前方一百米、只取一百米…...

TypeScript+React Web应用开发实战

&#x1f482; 个人网站:【 海拥】【神级代码资源网站】【办公神器】&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交流的小伙伴&#xff0c;请点击【全栈技术交流群】 在现代Web开发中&#xff0c;React和TypeScrip…...

android开发电子书,android基础编程

内存泄漏是什么&#xff1f; 内存泄漏即 ML &#xff08;Memory Leak&#xff09; 指 程序在申请内存后&#xff0c;当该内存不需再使用 但 却无法被释放 & 归还给 程序的现象 内存泄漏有哪些情况&#xff0c;对应的解决方案&#xff1f; 内存泄漏的原因归根到底就是当需…...

2024-02-25 Unity 编辑器开发之编辑器拓展6 —— Event

文章目录 1 Event 介绍2 重要 API3 代码示例 1 Event 介绍 ​ Event 提供许多属性和方法&#xff0c;允许检查和处理用户输入&#xff0c;主要用于 Unity 编辑器拓展开发。 ​ Input 相关内容需要在运行时才能监听输入&#xff0c;而 Event 专门提供给编辑模式下使用&#xf…...

DC-DC降压芯片用于直流充电桩,具备3A的输出电流能力,输入电压6~40VDC——D2576

随着新能源汽车的不断普及&#xff0c;如何解决新能源车充电的问题也成为大热话题&#xff0c;充电桩的数量与质量也是目前急需提升的热门方面&#xff0c;现阶段人们需要的充电桩主要有交流充电桩和直流充电桩&#xff0c;直流充电桩因其节能效率高、功率因数高、充电快、逐渐…...

4-如何进行细分市场的分析-02 细分行业的构成和基本情况

如何快速摸清行业的构成&#xff0c;通常会看同行或自己做过的相似的行业&#xff0c;会根据不同的行业来采用不同的研究方法。对于成熟的行业和不同的行业都会有一些比较通用的研究方式。 假设我们是在分析某一个行业&#xff0c;在分析行业的时候它的本质还是市场分析&#…...

L1-023 输出GPLT(PTA)

文章目录 输出GPLT题目描述代码 输出GPLT 题目描述 给定一个长度不超过10000的、仅由英文字母构成的字符串。请将字符重新调整顺序&#xff0c;按GPLTGPLT…这样的顺序输出&#xff0c;并忽略其它字符。当然&#xff0c;四种字符&#xff08;不区分大小写&#xff09;的个数不…...

【JavaEE进阶】 Spring AOP快速上手

文章目录 &#x1f343;什么是AOP&#x1f333;什么是Spring AOP&#x1f334;上手Spring AOP&#x1f6a9;引入依赖&#x1f6a9;编写AOP程序 ⭕总结 &#x1f343;什么是AOP AOP是Aspect Oriented Programming的简称&#xff08;又称为面向切⾯编程&#xff09; 什么是面向…...

android应用开发基础知识,安卓面试2020

第一章&#xff1a;设计思想与代码质量优化 1、设计思想六大原则 2、三大设计模式 3、数据结构 4、算法 第二章&#xff1a;程序性能优化 1、启动速度和执行效率优化 2、布局检测与优化 3、内存优化 4、耗电优化 5、网络传输与数据存储优化 6、APK大小优化 7、屏幕适配 8、…...

【根据当天日期输出明天的日期(需对闰年做判定)。】2022-5-15

缘由根据当天日期输出明天的日期(需对闰年做判定)。日期类型结构体如下&#xff1a; struct data{ int year; int month; int day;};-编程语言-CSDN问答 struct mdata{ int year; int month; int day; }mdata; int 天数(int year, int month) {switch (month){case 1: case 3:…...

【JavaEE】-- HTTP

1. HTTP是什么&#xff1f; HTTP&#xff08;全称为"超文本传输协议"&#xff09;是一种应用非常广泛的应用层协议&#xff0c;HTTP是基于TCP协议的一种应用层协议。 应用层协议&#xff1a;是计算机网络协议栈中最高层的协议&#xff0c;它定义了运行在不同主机上…...

Zustand 状态管理库:极简而强大的解决方案

Zustand 是一个轻量级、快速和可扩展的状态管理库&#xff0c;特别适合 React 应用。它以简洁的 API 和高效的性能解决了 Redux 等状态管理方案中的繁琐问题。 核心优势对比 基本使用指南 1. 创建 Store // store.js import create from zustandconst useStore create((set)…...

【WiFi帧结构】

文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成&#xff1a;MAC头部frame bodyFCS&#xff0c;其中MAC是固定格式的&#xff0c;frame body是可变长度。 MAC头部有frame control&#xff0c;duration&#xff0c;address1&#xff0c;address2&#xff0c;addre…...

DAY 47

三、通道注意力 3.1 通道注意力的定义 # 新增&#xff1a;通道注意力模块&#xff08;SE模块&#xff09; class ChannelAttention(nn.Module):"""通道注意力模块(Squeeze-and-Excitation)"""def __init__(self, in_channels, reduction_rat…...

华为云Flexus+DeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建

华为云FlexusDeepSeek征文&#xff5c;DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建 前言 如今大模型其性能出色&#xff0c;华为云 ModelArts Studio_MaaS大模型即服务平台华为云内置了大模型&#xff0c;能助力我们轻松驾驭 DeepSeek-V3/R1&#xff0c;本文中将分享如何…...

大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计

随着大语言模型&#xff08;LLM&#xff09;参数规模的增长&#xff0c;推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长&#xff0c;而KV缓存的内存消耗可能高达数十GB&#xff08;例如Llama2-7B处理100K token时需50GB内存&a…...

网站指纹识别

网站指纹识别 网站的最基本组成&#xff1a;服务器&#xff08;操作系统&#xff09;、中间件&#xff08;web容器&#xff09;、脚本语言、数据厍 为什么要了解这些&#xff1f;举个例子&#xff1a;发现了一个文件读取漏洞&#xff0c;我们需要读/etc/passwd&#xff0c;如…...

零知开源——STM32F103RBT6驱动 ICM20948 九轴传感器及 vofa + 上位机可视化教程

STM32F1 本教程使用零知标准板&#xff08;STM32F103RBT6&#xff09;通过I2C驱动ICM20948九轴传感器&#xff0c;实现姿态解算&#xff0c;并通过串口将数据实时发送至VOFA上位机进行3D可视化。代码基于开源库修改优化&#xff0c;适合嵌入式及物联网开发者。在基础驱动上新增…...

LCTF液晶可调谐滤波器在多光谱相机捕捉无人机目标检测中的作用

中达瑞和自2005年成立以来&#xff0c;一直在光谱成像领域深度钻研和发展&#xff0c;始终致力于研发高性能、高可靠性的光谱成像相机&#xff0c;为科研院校提供更优的产品和服务。在《低空背景下无人机目标的光谱特征研究及目标检测应用》这篇论文中提到中达瑞和 LCTF 作为多…...