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

Springboot 使用【过滤器】实现在请求到达 Controller 之前修改请求体参数和在结果返回之前修改响应体

文章目录

    • 前情提要
    • 解决方案
      • 自定义 HttpServletRequest 包装类 RequestWrapper
      • 自定义 HttpServletResponse 包装类 ResponseWrapper
      • 自定义过滤器 MiddlewareFilter
      • 配置过滤器
        • 注解
        • 配置类
      • 编写 Controller 测试

前情提要

在项目中需要使用过滤器 在请求调用 Controller 方法前修改请求参数和在结果返回之前修改返回结果

在 Controller 中定义如下接口:

@PostMapping("/hello")
public JSONObject hello(@RequestBody Map<String, Object> params) {return JSONObject.parseObject(JSON.toJSONString(params));
}

定义的过滤器如下:

public class ServNoFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {// 获取请求体内容String requestBody = getRequestBody(httpServletRequest);// 业务处理......// 放行filterChain.doFilter(httpServletRequest, httpServletResponse);}private String getRequestBody(HttpServletRequest request) throws IOException {BufferedReader reader = new BufferedReader(request.getReader());StringBuilder sb = new StringBuilder();String line;while ((line = reader.readLine()) != null) {sb.append(line);}return sb.toString();}
}

此时启动项目,访问接口,则会在控制台打印如下异常信息:Request processing failed; nested exception is java.lang.IllegalStateException: getReader() has already been called for this request

表示在过滤器中已经通过 request.getReader() 方法将请求流读取。

如果在过滤器中将 getReader() 换成 getInputStream() 就会报请求体为空异常:org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing

这是因为在 Servlet 中,请求对象的输入流只能被读取一次。而在第一次读取请求体时,Servlet 容器会将请求体保存在内存中,并将其解析成相应的请求参数和请求头信息。如果在后续的处理中再次读取请求体,就可能会导致数据错误或异常。

解决方案

自定义 HttpServletRequest 包装类 RequestWrapper

在 Servlet 中,原始的 HttpServletRequest 对象中的请求流(即请求体)只能读取一次。这是因为 HTTP 协议是基于流的协议,服务器在读取请求流时会将其消耗掉,一旦读取完毕,就无法再次读取

当 Servlet 容器读取完请求流后,会将请求的内容解析并储存在相应的属性中,如请求参数、请求头信息等。在后续的处理过程中,Servlet 可以从这些属性中获取请求内容,而不必再次读取请求流。

因此,我们需要自定义 RequestWrapper 将请求流保存下来,并提供方法来多次读取请求体的内容。

自定义 HttpServletRequest 包装类 RequestWrapper 如下:

/*** HttpServletRequest 包装类,允许在 Servlet 中多次读取请求体内容* 重写了 getInputStream()方法和 getReader() 方法,返回可以多次读取的流。*/
public class RequestWrapper extends HttpServletRequestWrapper {private final byte[] body;/*** 构造 RequestWrapper 对象** @param request 原始 HttpServletRequest 对象* @param context 请求体内容*/public RequestWrapper(HttpServletRequest request, String context) {super(request);this.body = context.getBytes(StandardCharsets.UTF_8);}/*** 重写 getInputStream 方法,返回经过包装后的 ServletInputStream 对象** @return 经过包装后的 ServletInputStream 对象*/@Overridepublic ServletInputStream getInputStream() {return new ServletInputStreamWrapper(new ByteArrayInputStream(body));}/*** 重写 getReader 方法,返回经过包装后的 BufferedReader 对象** @return 经过包装后的 BufferedReader 对象*/@Overridepublic BufferedReader getReader() {return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));}/*** 私有内部类,用于包装 ServletInputStream 对象*/private static class ServletInputStreamWrapper extends ServletInputStream {private final ByteArrayInputStream inputStream;/*** 构造函数,传入待包装的 ByteArrayInputStream 对象** @param inputStream 待包装的 ByteArrayInputStream 对象*/public ServletInputStreamWrapper(ByteArrayInputStream inputStream) {this.inputStream = inputStream;}/*** 重写 read 方法,读取流中的下一个字节** @return 读取到的下一个字节,如果已达到流的末尾,则返回-1*/@Overridepublic int read() {return inputStream.read();}/*** 覆盖 isFinished 方法,指示流是否已完成读取数据** @return 始终返回 false,表示流未完成读取数据*/@Overridepublic boolean isFinished() {return false;}/*** 重写 isReady 方法,指示流是否准备好进行读取操作** @return 始终返回 false,表示流未准备好进行读取操作*/@Overridepublic boolean isReady() {return false;}/*** 重写 setReadListener 方法,设置读取监听器** @param readListener 读取监听器*/@Overridepublic void setReadListener(ReadListener readListener) {}}
}

自定义 HttpServletResponse 包装类 ResponseWrapper

与请求流(即请求体)一样,原始的 HttpServletResponse 对象中的响应流(即响应体)只能写入一次。当服务器在向客户端发送响应时,会将响应流写入到网络传输通道中,一旦写入完毕,就无法再次修改或写入。

因此我们需要通过自定义 ResponseWrapper 包装原始的 HttpServletResponse 对象并重写其输出流或者输出写方法,从而实现对响应流的修改和控制。

自定义 HttpServletResponse 包装类 ResponseWrapper 如下:

/*** HttpServletResponse 包装类对,提供对响应数据的处理和操作。*/
public class ResponseWrapper extends HttpServletResponseWrapper {private final ByteArrayOutputStream outputStream;private ServletOutputStream servletOutputStream;private PrintWriter writer;/*** 构造函数,传入原始的 HttpServletResponse 对象** @param response 原始的 HttpServletResponse 对象*/public ResponseWrapper(HttpServletResponse response) {super(response);this.outputStream = new ByteArrayOutputStream();}/*** 重写 getOutputStream 方法,返回经过包装后的 ServletOutputStream 对象** @return 经过包装后的 ServletOutputStream 对象*/@Overridepublic ServletOutputStream getOutputStream() {if (servletOutputStream == null) {servletOutputStream = new ServletOutputStreamWrapper(outputStream);}return servletOutputStream;}/*** 重写 getWriter 方法,返回经过包装后的 PrintWriter 对象** @return 经过包装后的 PrintWriter 对象*/@Overridepublic PrintWriter getWriter() {if (writer == null) {writer = new PrintWriter(getOutputStream());}return writer;}/*** 获取响应数据,并指定字符集** @param charsetName 字符集名称* @return 响应数据字符串*/public String getResponseData(String charsetName) {Charset charset = Charset.forName(charsetName);byte[] bytes = outputStream.toByteArray();return new String(bytes, charset);}/*** 设置响应数据,并指定字符集** @param responseData 响应数据字符串* @param charsetName  字符集名称*/public void setResponseData(String responseData, String charsetName) {Charset charset = Charset.forName(charsetName);byte[] bytes = responseData.getBytes(charset);outputStream.reset();try {outputStream.write(bytes);} catch (IOException e) {// 处理异常}setCharacterEncoding(charsetName);}/*** 私有内部类,用于包装 ServletOutputStream 对象*/private static class ServletOutputStreamWrapper extends ServletOutputStream {private final ByteArrayOutputStream outputStream;/*** 构造函数,传入待包装的 ByteArrayOutputStream 对象** @param outputStream 待包装的 ByteArrayOutputStream 对象*/public ServletOutputStreamWrapper(ByteArrayOutputStream outputStream) {this.outputStream = outputStream;}/*** 重写 write 方法,将指定字节写入输出流** @param b 字节*/@Overridepublic void write(int b) {outputStream.write(b);}/*** 重写 isReady 方法,指示输出流是否准备好接收写入操作** @return 始终返回 false,表示输出流未准备好接收写入操作*/@Overridepublic boolean isReady() {return false;}/*** 重写 setWriteListener 方法,设置写入监听器** @param writeListener 写入监听器*/@Overridepublic void setWriteListener(WriteListener writeListener) {}}
}

自定义过滤器 MiddlewareFilter

我们的需求是:在请求到达服务器之前,对请求参数进行修改;在响应返回之前,对响应结果进行处理。

对于这样的需求,我们可以通过自定义过滤器来实现。大致实现思路如下:

  • 修改请求参数(请求体),我们可以:

    1. 获取请求体内容。
    2. 修改请求体内容。
    3. 将修改后的请求对象替换原来的请求对象,以便后续获取修改后的参数。
  • 修改响应结果(响应体),我们可以:

    1. 获取响应数据。
    2. 对响应数据进行处理。
    3. 将修改后的数据作为最终结果返回。

同时为了确保每个请求在请求时只会被过滤一次,我们可以继承 OncePerRequestFilter 来定义自己的过滤器。

最终,自定义过滤器如下:

public class MiddlewareFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {// 1. 从 HttpServletRequest 对象中获取请求体内容String requestBody = getRequestBody(httpServletRequest);// 2. 解析请求体内容为JSON对象JSONObject jsonBody = JSONObject.parseObject(requestBody);// 3. 修改请求体内容jsonBody.put("paramKey","paramValue");// 4. 包装 HttpServletRequest 对象为自定义的 RequestWrapper 对象,以便后续的处理RequestWrapper requestWrapper = new RequestWrapper(httpServletRequest, jsonBody.toJSONString());// 5. 包装 HttpServletResponse 对象为自定义的 ResponseWrapper 对象,以便后续的处理ResponseWrapper responseWrapper = new ResponseWrapper(httpServletResponse);// 6. 调用下一个过滤器或 ServletfilterChain.doFilter(requestWrapper, responseWrapper);// 7. 获取响应数据String responseData = responseWrapper.getResponseData(StandardCharsets.UTF_8.name());// 8. 解析响应数据为JSON对象JSONObject jsonData = JSONObject.parseObject(responseData);// 9. 在这里可以对响应数据进行处理jsonData.put("responseKey", "responseValue");// 10. 将修改后的 JSON 对象转换为字符串responseData = jsonData.toJSONString();// 11. 将修改后的 JSON 对象设置为最终的响应数据responseWrapper.setResponseData(responseData, StandardCharsets.UTF_8.name());// 12. 将响应数据写入原始的响应对象,解决响应数据无法被多个过滤器处理问题OutputStream outputStream = httpServletResponse.getOutputStream();outputStream.write(responseData.getBytes(StandardCharsets.UTF_8));outputStream.flush();}/*** 获取请求体内容。** @param request HttpServletRequest对象* @return 请求体内容* @throws IOException 如果读取请求体内容时发生I/O异常*/private String getRequestBody(HttpServletRequest request) throws IOException {BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));StringBuilder sb = new StringBuilder();String line;while ((line = reader.readLine()) != null) {sb.append(line);}return sb.toString();}
}

配置过滤器

注解

通过 Java Servlet 3.0 规范中引入的 @WebFilter 注解配置过滤器。

@WebFilter 注解可以应用在实现了 Filter 接口或继承自 OncePerRequestFilter 的类上,标识该类为过滤器,并指定过滤器的相关配置,包括拦截的 URL 路径、执行顺序以及初始化参数等。

我们可以在 MiddlewareFilter 过滤器上使用 @WebFilter 注解注册该过滤器并指定执行该过滤器执行的顺序和拦截的 URL:

@WebFilter(value = "1000", urlPatterns = "/hello")
public class MiddlewareFilter extends OncePerRequestFilter {......
}
  • value:设置过滤器的执行顺序,数字越小,优先级越高。
  • urlPatterns:指定要拦截的 URL 路径,允许指定多个 URL 路径urlPatterns = {"/hello","/hello1"}

还需要再启动类上使用@ServletComponentScan注解扫描和注册带有 @WebServlet@WebFilter@WebListener 注解的组件:

@ServletComponentScan
@SpringBootApplication
public class Demo1Application {public static void main(String[] args) {SpringApplication.run(Demo1Application.class, args);}}
配置类

除了注解的形式配置过滤器,我们还可以通过配置类的形式进行配置。

创建 FilterConfig 类用于配置需要注册的过滤器,同时在类上添加 @Configuration 注解,标识该类为配置类,在项目启动时 Spring 会自动扫描该类中的 Bean 定义,并将其加载到容器中:

@Configuration
public class FilterConfig {@Beanpublic FilterRegistrationBean<MiddlewareFilter> middlewareFilter() {FilterRegistrationBean<MiddlewareFilter> registration = new FilterRegistrationBean<>();registration.setFilter(new MiddlewareFilter()); // 设置过滤器实例registration.addUrlPatterns("/hello"); // 拦截的 URL 路径registration.setOrder(1000); // 设置过滤器执行顺序(数字越小,越先执行)return registration;}}

在类中我们定义了名为 middlewareFilter 的方法,用于注册我们自定义的 MiddlewareFilter 过滤器。

在 方法中,创建了一个 FilterRegistrationBean 对象用于注册和配置过滤器,并设置 MiddlewareFilter 对象作为过滤器实例,指定了过滤器要拦截的 URL 路径,滤器执行顺序。

最后将 FilterRegistrationBean 对象返回,以便 Spring 自动进行注册和管理。

编写 Controller 测试

创建两个接口,同样的逻辑,接收一个请求体参数 params,再将接收的参数以 JSON 格式返回:

@RestController
public class BasicController {/*** 处理 /hello 请求的方法* @param params 请求体参数,以键值对的形式传递* @return 经过转换后的 JSONObject 对象*/@PostMapping("/hello")public JSONObject hello(@RequestBody Map<String, Object> params) {return JSONObject.parseObject(JSON.toJSONString(params));}@PostMapping("/hello1")public JSONObject hello1(@RequestBody Map<String,Object> params) {return JSONObject.parseObject(JSON.toJSONString(params));}
}

启动项目,在 ApiFox 中分别以同样的请求参数发送 POST 请求调用 /hello/hello1 接口:

  • 请求参数:

    {"name": "hello","age": 20
    }
    
  • /hello 接口返回结果:

    {"paramKey": "paramValue","responseKey": "responseValue","name": "hello","age": 20
    }
    
  • /hello1 接口返回结果:

    {"name": "hello","age": 20
    }
    

复制多个 MiddlewareFilter 过滤器模拟多层过滤器修改请求体参数和返回结果,测试结果如下:

{"paramKey": "paramValue",	//过滤器1"responseKey2": "responseValue2",	//过滤器2"responseKey": "responseValue",	//过滤器2"paramKey2": "paramValue2",	//过滤器1"name": "hello","age": 20
}

相关文章:

Springboot 使用【过滤器】实现在请求到达 Controller 之前修改请求体参数和在结果返回之前修改响应体

文章目录 前情提要解决方案自定义 HttpServletRequest 包装类 RequestWrapper自定义 HttpServletResponse 包装类 ResponseWrapper自定义过滤器 MiddlewareFilter配置过滤器注解配置类 编写 Controller 测试 前情提要 在项目中需要使用过滤器 在请求调用 Controller 方法前修改…...

Unity 3D常用的数据结构

目录 数组使用场景 ArrayList数组ArrayList的缺点 List\<T\>数组List\<T\>有以下3点好处 链表链表与数组的不同之处链表的优势数组和链表的应用场景 LinkedList\<T\>C#中内置的双向链表LinkedList使用场景 队列&#xff08;Queue\<T\>&#xff09;和栈…...

h5唤起微信小程序

wx-open-launch-weapp 就用这个 开放标签属于自定义标签&#xff0c;Vue会给予未知标签的警告&#xff0c;可通过配置Vue.config.ignoredElements [wx-open-launch-weapp] 来忽略Vue对开放标签的检查。 sdk授权。 调试打开时iOS会弹窗 noPermissionJsApi: []&#xff0c;confi…...

面向对象(精髓)变继承关系为组和关系(_Decorator模式)

在软件开发中&#xff0c;设计模式是解决常见问题的可重用解决方案。在面向对象编程中&#xff0c;继承和组合是两种常用的代码复用方式。然而&#xff0c;随着软件需求的不断变化&#xff0c;我们需要更灵活的设计方式来应对不断变化的需求。在本文中&#xff0c;我们将讨论从…...

MES系统在智能生产中的重要作用

在未来智能制造的发展趋势中&#xff0c;制造执行系统&#xff08;MES&#xff09;作为关键技术和工具&#xff0c;扮演着至关重要的角色。随着科技的不断进步和制造业的数字化转型&#xff0c;MES的地位将愈发凸显&#xff0c;对于企业实现智能化生产、提高效率、降低成本具有…...

2024.3.13每日一题

LeetCode 最大二进制奇数 题目链接&#xff1a;2864. 最大二进制奇数 - 力扣&#xff08;LeetCode&#xff09; 题目描述 给你一个 二进制 字符串 s &#xff0c;其中至少包含一个 1 。 你必须按某种方式 重新排列 字符串中的位&#xff0c;使得到的二进制数字是可以由该组…...

YOLOv5 | 涨点复现!YOLOv5添加BiFPN有效提升目标检测精度

目录 &#x1f680;&#x1f680;&#x1f680;订阅专栏&#xff0c;更新及时查看不迷路&#x1f680;&#x1f680;&#x1f680; 介绍&#xff1a; BiFPN 代码实现 ⭐欢迎大家订阅我的专栏一起学习⭐ &#x1f680;&#x1f680;&#x1f680;订阅专栏&#xff0c;更新及…...

【Nut3】nuxt.config.ts项目nuxt配置文件介绍

简言 记录下nuxt3的nuxt.config.ts文件的介绍和使用。 Nuxt Configuration nuxt.config.ts Nuxt可以通过一个单独的nuxt.config文件进行简单配置。 配置文件创建 nuxt.config文件的扩展名可以是.js、.ts或.mjs。 然后默认导出全局函数defineNuxtConfig的返回值&#xff0c…...

区块链技术的革命性影响

1. 区块链技术的基本原理&#xff1a; 区块链是一种去中心化的分布式数据库技术&#xff0c;通过不断增长的记录&#xff08;块&#xff09;构成一个链式结构。每个区块包含了交易数据的加密信息以及上一个区块的哈希值&#xff0c;从而形成了不可篡改的交易记录。这种去中心化…...

多线程(volatile)

volatile的功能 保证内存可见性禁止指令重排序 内存可见性 简单的理解 两(多)个线程同时针对一个变量进行操作, 一个线程读, 一个线程修改, 此时读到的值不一定是修改过后的值 即读线程没有感知到变量的变化 (其实是 编译器/JVM 对于代码在多线程情况下的优化进行了误判) 从 J…...

蓝桥杯 填空 卡片

蓝桥杯 填空题 卡片 解题思路&#xff1a; 我们只需要消耗完卡片的个数即可。 代码示例&#xff1a; #include<bits/stdc.h> using namespace std; int a[10]; bool isEnd(){for(int i0;i<10;i){if(a[i]-1)return false;}return true; } bool getN(int x){while(x){i…...

ELK介绍使用

文章目录 一、ELK介绍二、Elasticsearch1. ElasticSearch简介&#xff1a;2. Elasticsearch核心概念3. Elasticsearch安装4. Elasticsearch基本操作1. 字段类型介绍2. 索引3. 映射4. 文档 5. Elasticsearch 复杂查询 三、LogStash1. LogStash简介2. LogStash安装 四、kibana1. …...

【UE5】非持枪状态蹲姿移动的动画混合空间

项目资源文末百度网盘自取 在BlendSpace文件夹中单击右键选择动画(Animation)中的混合空间(Blend Space) &#xff0c;选择SK_Female_Skeleton&#xff0c;命名为BS_NormalCrouch 打开BS_NormalCrouch 水平轴表示角色的方向&#xff0c;命名为Direction&#xff0c;方向的最…...

Windows C++ TCP开发(使用select函数以及设置非阻塞/Reuse属性)

1、select官方函数说明&#xff1a; 语法 C int WSAAPI select([in] int nfds,[in, out] fd_set *readfds,[in, out] fd_set *writefds,[in, out] fd_set *exceptfds,[in] const timeval *timeout );参数 [in] nfds 已忽略。 包含 nf…...

ARM TrustZone技术解析:构建嵌入式系统的安全扩展基石

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法|MySQL| ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-LOdvohfCEnd8eKyd {font-family:"trebuchet ms",verdana,arial,sans-serif;f…...

初识Python语言-课堂练习【pyhton123题库】

初识Python语言-课堂练习【pyhton123题库】 一、单项选择题 1、Guido van Rossum正式对外发布Python版本的年份是&#xff1a; A 2008B 1998C 1991D 2002 【答案】C 【解析】暂无解析2、下面不是Python语言特点的是&#xff1a;‪‬‪‬‪‬‪‬‪‬‮‬‪‬‫‬‪‬‪‬‪…...

chrome高内存占用问题

chrome号称内存杀手不是盖的&#xff0c;不设设置的话&#xff0c;经常被它内存耗尽死机是常事。以下自用方法 1 自带的memory saver chrome://settings/performance PerformanceMemory Saver When on, Chromium frees up memory from inactive tabs. This gives active tab…...

【C语言】文件操作篇-----程序文件和数据文件,文件的打开和关闭,二进制文件和文本文件,fopen,fclose【图文详解】

欢迎来CILMY23的博客喔&#xff0c;本篇为【C语言】文件操作篇-----程序文件和数据文件&#xff0c;文件的打开和关闭&#xff0c;二进制文件和文本文件【图文详解】&#xff0c;感谢观看&#xff0c;支持的可以给个一键三连&#xff0c;点赞关注收藏。 前言 在了解完动态内存管…...

知识碎片收集

目录 1. 如何计算两点经纬度之间的距离2. 加权随机采样3.什么时LLDB和GDB 1. 如何计算两点经纬度之间的距离 1.知乎-如何计算两点经纬度间距离 2.根据两点经纬度坐标计算距离 3.根据经纬度计算两点之间的距离的公式推导过程以及google.maps的测距函数 4.根据经纬度点计算经…...

不可不知!用例图的绘制与应用全指南深度解析

在软件开发领域中&#xff0c;用例图是一种强大的工具&#xff0c;用于描述系统的功能需求以及系统与外部实体之间的交互。无论是在需求分析阶段还是在系统设计过程中&#xff0c;用例图都扮演着至关重要的角色。本文将全面介绍用例图的绘制方法和其在软件开发中的应用&#xf…...

Apache Mesos vs Kubernetes:如何选择最适合你的容器编排平台 [特殊字符]

Apache Mesos vs Kubernetes&#xff1a;如何选择最适合你的容器编排平台 &#x1f680; 【免费下载链接】mesos apache/mesos: 这是一个开源的集群管理框架&#xff0c;用于在异构资源池上部署和管理应用程序。它允许开发者使用高效的资源隔离和共享机制&#xff0c;构建高度可…...

硬件工程师的福音:用Beyond Compare 4表格比对功能,5分钟搞定BOM清单版本差异检查

硬件工程师的效率革命&#xff1a;Beyond Compare 4表格比对功能深度解析 在硬件研发的日常工作中&#xff0c;BOM清单的版本管理往往是最令人头疼的环节之一。每次PCB设计的小版本迭代——无论是物料替换、数量调整还是参数优化——都需要工程师花费大量时间核对变更细节。传统…...

如何实现 SEO 优化与广告投放的协同效果

如何实现 SEO 优化与广告投放的协同效果 在当今竞争激烈的互联网环境中&#xff0c;实现 SEO 优化与广告投放的协同效果已经成为提升网站流量和销售转化的关键。SEO&#xff08;搜索引擎优化&#xff09;和广告投放虽然各自有各自的目标和优势&#xff0c;但它们的结合能够带来…...

为什么你的C++量子模拟器总在2^10后崩溃?内存优化、张量压缩与SIMD加速三重方案揭秘

第一章&#xff1a;量子模拟器崩溃现象与2^10内存临界点的本质剖析当量子模拟器在经典硬件上运行含10个量子比特的电路时&#xff0c;常在初始化或状态演化阶段发生静默崩溃——进程异常终止、无堆栈回溯、仅返回 SIGSEGV 或 OOM Killer 日志。这一现象并非随机故障&#xff0c…...

Git從入門到「入坑」:一個新手的環境配置與踩坑實錄

Git從入門到「入坑」&#xff1a;一個新手的環境配置與踩坑實錄 ——AtomGit春季徵稿開源入門實戰分享 導語&#xff1a;為什麼我要寫這篇文章&#xff1f; 三個月前&#xff0c;我連git clone和git pull的區別都說不清楚。每次看到Git報錯&#xff0c;我的第一反應不是讀錯誤信…...

2026年AI风口已至!月薪3万+岗位盘点+零基础转行指南,速收藏!

本文详细介绍了2026年转行AI的优势与机遇&#xff0c;指出行业人才缺口巨大且薪资水平高。文章全面梳理了AI行业的各类岗位&#xff0c;并针对技术、产品、运营、培训等不同转行路径&#xff0c;提供了分阶段的学习指南和推荐资源。此外&#xff0c;还针对应届毕业生、传统行业…...

CDA Level-2 考试全攻略:从报名到备考的保姆级教程(含最新题库资源)

CDA Level-2 考试全攻略&#xff1a;从报名到备考的保姆级教程 最近两年数据分析师认证热度持续攀升&#xff0c;CDA认证作为国内认可度较高的专业证书之一&#xff0c;Level-2考试通过率常年维持在40%左右。不同于Level-1的基础考核&#xff0c;Level-2更注重实际分析能力与统…...

小智AI固件开发者的福音:VSCode插件一键搞定ESP-IDF v5.4环境(Windows/Linux通用)

小智AI固件开发者的福音&#xff1a;VSCode插件一键搞定ESP-IDF v5.4环境&#xff08;Windows/Linux通用&#xff09; 在物联网开发领域&#xff0c;ESP32系列芯片凭借其优异的性能和丰富的功能&#xff0c;已经成为智能硬件开发的首选平台之一。而作为ESP32官方推荐的开发框架…...

医疗AI实战:如何用NLP技术从电子病历中提取科研特征(附Python代码)

医疗AI实战&#xff1a;从电子病历中挖掘科研金矿的NLP技术指南 在医疗健康领域&#xff0c;电子病历&#xff08;EMR&#xff09;是一座尚未充分开发的数据金矿。据统计&#xff0c;医疗机构产生的数据中超过70%是非结构化文本信息&#xff0c;包括医生记录、检查报告和病程描…...

新手必看:用Wireshark分析CTF流量题,手把手教你从抓包到找到Flag

从零玩转Wireshark&#xff1a;CTF流量分析实战指南 第一次打开Wireshark时&#xff0c;满屏跳动的数据包就像天书一样让人头晕目眩。但别担心&#xff0c;每个网络安全高手都曾经历过这个阶段。本文将带你走进CTF流量分析的世界&#xff0c;从最基础的Wireshark操作开始&#…...