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

SpringBoot(五)--- 异常处理、JWT令牌、拦截技术

目录

一、异常处理

1. 问题分析

2. 全局异常处理器

二、登录校验技术

1.会话技术

1.1 Cookie

1.2 Session

2. JWT令牌

2.1 介绍

2.2 生成与校验

2.3 登录时下发令牌

3. 过滤器Filter

3.1 概述

3.2 登录校验过滤器

3.3 Filter详解

4. 拦截器interceptor

4.1 令牌校验interceptor

4.2 拦截路径

4.3 执行流程

5. 拦截器与过滤器的区别

一、异常处理

1. 问题分析

现在有一张数据库表emp,表示员工表。其中的电话号码phone字段添加了唯一约束。

当我们在修改数据时,如果输入了一个在数据库表中已经存在的手机号,点击保存按钮之后,前端提示了错误信息,但是返回的结果并不是统一的响应结果,而是框架默认返回的错误结果 (系统接口访问异常)。

规定服务端给前端返回的数据结果包装为一个Result类:

/*** 后端统一返回结果*/
@Data
public class Result {private Integer code; //编码:1成功,0为失败private String msg; //错误信息private Object data; //数据public static Result success() {Result result = new Result();result.code = 1;result.msg = "success";return result;}public static Result success(Object object) {Result result = new Result();result.data = object;result.code = 1;result.msg = "success";return result;}public static Result error(String msg) {Result result = new Result();result.msg = msg;result.code = 0;return result;}}

状态码为500,表示服务器端异常。但是服务器端向前端返回的数据并不符合我们定义的Result。由于返回的数据不符合开发规范,所以前段并不能解析出响应的JSON数据。

服务器端表示emp表中的phone这个字段重复了。

出现异常之后,项目并没有做任何的异常处理。

当我们没有做任何的异常处理时,我们三层架构处理异常的方案:

  • Mapper接口在操作数据库的时候出错了,此时异常会往上抛(谁调用Mapper就抛给谁),会抛给service。

  • service 中也存在异常了,会抛给controller。

  • 而在controller当中,我们也没有做任何的异常处理,所以最终异常会再往上抛。最终抛给框架之后,框架就会返回一个JSON格式的数据,里面封装的就是错误的信息,但是框架返回的JSON格式的数据并不符合我们的开发规范。

2. 全局异常处理器

我们该怎么样定义全局异常处理器?

  • 定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解@RestControllerAdvice,加上这个注解就代表我们定义了一个全局异常处理器。

  • 在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。如果value属性未指定具体值,则会根据方法中的形参来捕获异常。下面方法中形参类型为Exception,因此会捕获所有类型的异常。

  • 只要项目中代码运行出现相关类型的异常,全局异常处理器就会捕获相关的异常,然后向前端响应方法中定义的相关的信息。

@RestControllerAdvice
public class GlobalExceptionHandler {//处理异常@ExceptionHandlerpublic Result ex(Exception e){//方法形参中指定能够处理的异常类型e.printStackTrace();//打印堆栈中的异常信息//捕获到异常之后,响应一个标准的Resultreturn Result.error("对不起,操作失败,请联系管理员");}}

重新启动SpringBoot服务,打开浏览器,再来测试一下 修改员工 这个操作,我们依然设置已存在的手机号:

此时,我们可以看到,出现异常之后,异常已经被全局异常处理器捕获了。然后返回的错误信息,被前端程序正常解析,然后提示出了对应的错误提示信息。

以上就是全局异常处理器的使用,主要涉及到两个注解:

  • @RestControllerAdvice //表示当前类为全局异常处理器

  • @ExceptionHandler //指定可以捕获哪种类型的异常进行处理

二、登录校验技术

所谓登录校验,指的是我们在服务器端接收到浏览器发送过来的请求之后,首先我们要对请求进行校验。先要校验一下用户登录了没有,如果用户已经登录了,就直接执行对应的业务操作就可以了;如果用户没有登录,此时就不允许他执行相关的业务操作,直接给前端响应一个错误的结果,最终跳转到登录页面,要求他登录成功之后,再来访问对应的数据。

HTTP协议是一种无状态的协议,所谓无状态,指的就是每一次请求都是独立的,下一次请求不会携带上一次请求的数据。而浏览器与服务器之间进行交互,基于HTTP协议也就意味着现在我们通过浏览器来访问了登陆这个接口,实现了登陆的操作,接下来我们在执行其他业务操作时,服务器也并不知道这个员工到底登陆了没有。因为HTTP协议是无状态的,两次请求之间是独立的,所以是无法判断这个员工到底登陆了没有。

那应该怎么来实现登录校验的操作呢?具体的实现思路可以分为两部分:

  1. 在员工登录成功后,需要将用户登录成功的信息存起来,记录用户已经登录成功的标记。

  2. 在浏览器发起请求时,需要在服务端进行统一拦截,拦截后进行登录校验。

当用户第一次进行登录请求时,服务器端会生成一个登录标记,然后将登录标记一起响应给浏览器。当再进行其他请求时,前端会携带着登录标记一起发送给服务器端,如果服务器端有这个登录标记,就给前端相应相应的请求。

1.会话技术

在web开发中,会话指的就是浏览器和服务器之间的一次连接,就称为一次会话。在用户打开浏览器第一次访问服务器的时候,这个会话就建立了,直到有任何一方断开连接,此时会话就结束了。在一次会话当中,是可以包含多次请求和响应的。

需要注意的是:会话是和浏览器关联的,当有三个浏览器客户端和服务器建立了连接时,就会有三个会话。同一个浏览器在未关闭之前请求了多次服务器,这多次请求是属于同一个会话。比如:1、2、3这三个请求都是属于同一个会话。当我们关闭浏览器之后,这次会话就结束了。而如果我们是直接把web服务器关了,那么所有的会话就都结束了。

会话跟踪:服务器需要识别多次请求是否来自同一个浏览器,以便在同一次会话的多个请求间共享数据。

会话跟踪方案有两种:

  1. Cookie(客户端会话跟踪技术):数据存储在客户端浏览器当中

  2. Session(服务端会话跟踪技术):数据存储在储在服务端

1.1 Cookie

Cookie是存储在客户端浏览器的,当我们在浏览器第一次向服务器发送请求时,就在服务器端来设置一个Cookie。

比如第一次请求了登录接口,登录接口执行完成之后,我们就可以设置一个cookie,在 cookie 当中我们就可以来存储用户相关的一些数据信息。比如我可以在 cookie 当中来存储当前登录用户的用户名,用户的ID。

服务器端在向客户端相应数据时,会自动的将Cookie响应给浏览器,浏览器接收到Cookie之后,会自动的将Cookie存储在本地。之后的每一次请求中,浏览器就会自动的将本地存储的Cookie带到服务器端。

服务器端接收到这个Cookie,就回去判断这个Cookie值是否存在,如果不存在,说明没有进行登录操作;如果存在,就说明该客户端已经连接成功,就可以基于Cookie在一次会话中的不同请求之间来共享数据。

这一切都是自动进行的,因为 cookie 它是 HTTP 协议当中所支持的技术,而各大浏览器厂商都支持了这一标准。在 HTTP 协议官方给我们提供了一个响应头和请求头:

  • 响应头 Set-Cookie :设置Cookie数据的

  • 请求头 Cookie:携带Cookie数据的

优缺点:

  • 优点:HTTP协议中支持的技术(像Set-Cookie 响应头的解析以及 Cookie 请求头数据的携带,都是浏览器自动进行的,是无需我们手动操作的)

  • 缺点:

    • 移动端APP(Android、IOS)中无法使用Cookie

    • 不安全,用户可以自己禁用Cookie

    • Cookie不能跨域,ip地址的协议不同、地址不同、端口不同都算跨域。只有协议、地址、端口都相同时,才算同一个域,例如:http://192.168.150.200/login.html ----------> http://192.168.150.200/login。

大白话来解释:假如你第一次去咖啡店,店员送你一张「积分卡」,上面记录你买了几杯咖啡(比如买5杯送1杯)。下次你去店里,只要出示这张卡,店员就知道你的消费记录。Cookie技术——你的消费数据存在你手里(浏览器里),每次访问网站都会自动带上这个“积分卡”给服务器看。

1.2 Session

Session是服务器端跟踪技术,Session的底层就是基于Cookie来实现的。

浏览器在第一次请求服务器时,会话对象是不存在的,服务器就会自动创建一个会话对象Session。每一个会话对象Session都会有一个ID(示意图中Session后面括号中的1,就表示ID)。

然后,服务器在向浏览器响应数据时,会将Session的ID通过Cookie响应给浏览器。然后浏览器会将Cookie保存在本地,Cookie中有Session的ID数据。

在之后的请求中,每次都会将Session的ID随着Cookie一起发送给服务器端,服务器端接收到ID,就会从众多的会话对象Session中找到当前ID的会话对象。从而进行数据共享。

优缺点

  • 优点:Session是存储在服务端的,安全

  • 缺点:

    • 服务器集群环境下无法直接使用Session,1号服务器知道Session的ID,但是2号服务器并不知道

    • 移动端APP(Android、IOS)中无法使用Cookie

    • 用户可以自己禁用Cookie

    • Cookie不能跨域

大白话解释:还是同一家咖啡店,店员并不给你积分卡,而是给你一张「号码牌」,然后在店里的本子上用这个号码记录你的消费次数。你只需要记住号码牌,实际数据存在店里(服务器)。Session技术——服务器给你一个唯一的ID(比如JSESSIONID),存在你的Cookie里;服务器用这个ID在自己的“小本本”里查你的数据。

2. JWT令牌

令牌就是一个用户身份的标识,本质就是一段字符串。

当浏览器端进行登录请求的时候,服务器端就会生成一个令牌,响应给前端。然后前端程序就会将这个令牌存储起来,可以存储在 cookie 当中,也可以存储在其他的存储空间(比如:localStorage)当中。之后的每次请求,都需要将令牌携带到服务器端,服务器端对令牌进行校验,如果合法就放行。

优缺点

  • 优点:

    • 支持PC端、移动端

    • 解决集群环境下的认证问题

    • 减轻服务器的存储压力(无需在服务器端存储)

  • 缺点:需要自己实现(包括令牌的生成、令牌的传递、令牌的校验)

大白话解释:想象你入住酒店时,前台给你一张「房卡」。这张房卡本身不值钱,但它能证明你是住客,能刷开对应楼层的电梯和房间门。​令牌(Token)​就是这个原理——它本质上是一个经过加密的字符串,用来代替真实的账户密码,证明"你是谁"以及"你能做什么"。

2.1 介绍

JWT的组成: (JWT令牌由三个部分组成,三个部分之间使用英文的点来分割)

  • 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{"alg":"HS256","type":"JWT"}

  • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{"id":"1","username":"Tom"}

  • 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。这一部分是通过前两个部分自动生成的。

JWT令牌是通过base64编码形式将JSON形式的数据转换为字符串的。

2.2 生成与校验

想使用JWT令牌,就需要首先引入JWT的依赖。

<!-- JWT依赖-->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version>
</dependency>

生成JWT代码实现:

@Test
public void testGenJwt() {Map<String, Object> claims = new HashMap<>();claims.put("id", 10);claims.put("username", "itheima");// signWith方式指定签名算法(HS256)以及密钥(aXRjYXN0)// 密钥是自己进行定义,然后通过base64编码形成的String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, "aXRjYXN0")// addClaims方法用来添加自定义数据,形参的形式为Map.addClaims(claims)// setExpirationy用来定义JWT令牌的有效时间,一旦有效时间过了,就需要重新申请令牌.setExpiration(new Date(System.currentTimeMillis() + 12 * 3600 * 1000))// compact方法用于构建令牌.compact();System.out.println(jwt);
}

测试方法运行结果如下:

eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjcyNzI5NzMwfQ.fHi0Ub8npbyt71UqLXDdLyipptLgxBUg_mSuGJtXtBk

解析JWT令牌代码如下:

@Test
public void testParseJwt() {// parser方法说明要解析令牌,setSigningKey方法是指定密钥,该密钥必须和生成的时候密钥完全一致Claims claims = Jwts.parser().setSigningKey("aXRjYXN0")//parseClaimsJws方法是将你的JWT令牌进行输入
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAsInVzZXJuYW1lIjoiaXRoZWltYSIsImV4cCI6MTcwMTkwOTAxNX0.N-MD6DmoeIIY5lB5z73UFLN9u7veppx1K5_N_jS9Yko")// getBody方法是获取令牌的第二部分,也就是自定义数据部分.getBody();System.out.println(claims);
}

运行测试方法结果如下:

{id=10, username=itheima, exp=1701909015}

与我们自己定义的数据是一致的。

篡改令牌中的任何一个字符,在对令牌进行解析时都会报错,所以JWT令牌是非常安全可靠的。

2.3 登录时下发令牌

在登录成功之后来生成一个JWT令牌,并且把这个令牌直接返回给前端。

根据接口文档,先有一个实体类LoginInfo用于接收用户登录传递给服务器的信息,然后服务器端再响应给前端。

public class LoginInfo {private Integer id;       // 用户idprivate String username;  // 用户的用户名private String name;      // 用户的姓名private String token;     // JWT令牌
}

我们先将生成与校验令牌的代码定义为一个工具类:

package com.itheima.util;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;import java.util.Date;
import java.util.Map;public class JwtUtils {private static String signKey = "SVRIRUlNQQ==";private static Long expire = 43200000L;/*** 生成JWT令牌* @return*/public static String generateJwt(Map<String,Object> claims){String jwt = Jwts.builder().addClaims(claims).signWith(SignatureAlgorithm.HS256, signKey).setExpiration(new Date(System.currentTimeMillis() + expire)).compact();return jwt;}/*** 解析JWT令牌* @param jwt JWT令牌* @return JWT第二部分负载 payload 中存储的内容*/public static Claims parseJWT(String jwt){Claims claims = Jwts.parser().setSigningKey(signKey).parseClaimsJws(jwt).getBody();return claims;}
}

然后在登录逻辑的时候下发JWT令牌:

@Override
public LoginInfo login(Emp emp) {// 根据用户的用户名和密码来获得用户信息Emp empLogin = empMapper.getUsernameAndPassword(emp);// 如果存在该用户if(empLogin != null){// 自定义信息,将用户id和用户名存入JWT令牌中Map<String,Object> dataMap = new HashMap<>();dataMap.put("id", empLogin.getId());dataMap.put("username", empLogin.getUsername());// 生成令牌String jwt = JwtUtils.generateJwt(dataMap);// 将用户信息和令牌信息存入LoginInfo对象当中,返回给前端LoginInfo loginInfo = new LoginInfo(empLogin.getId(), empLogin.getUsername(), empLogin.getName(), jwt);return loginInfo;}return null;
}

3. 过滤器Filter

3.1 概述

Filter表示过滤器,是 JavaWeb三大组件(Servlet、Filter、Listener)之一。过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。使用了过滤器之后,要想访问web服务器上的资源,必须先经过滤器,过滤器处理完毕之后,才可以访问对应的资源。

使用过滤器的基本操作如下:

  • 第1步,定义过滤器 :定义一个类,实现 Filter 接口,并重写其所有方法。

  • 第2步,配置过滤器:Filter类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加 @ServletComponentScan 开启Servlet组件支持。

定义一个类,实现Filter接口:

@WebFilter(urlPatterns = "/*") //配置过滤器要拦截的请求路径( /* 表示拦截浏览器的所有请求 )
public class DemoFilter implements Filter {//初始化方法, web服务器启动, 创建Filter实例时调用, 只调用一次public void init(FilterConfig filterConfig) throws ServletException {System.out.println("init ...");}//拦截到请求时,调用该方法,可以调用多次public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {System.out.println("拦截到了请求...");}//销毁方法, web服务器关闭时调用, 只调用一次public void destroy() {System.out.println("destroy ... ");}
}

启动类上加上@ServletComponentScan注解,表示这个启动类执行的web项目所发出的请求都会被拦截:

@ServletComponentScan //开启对Servlet组件的支持
@SpringBootApplication
public class TliasManagementApplication {public static void main(String[] args) {SpringApplication.run(TliasManagementApplication.class, args);}
}

启动服务,执行请求,控制台给出了过滤器的信息:

3.2 登录校验过滤器

用户在进行登录操作时,服务器端会给前端下发一个令牌。然后在后续的每一次操作中,都需要将令牌下发到服务器端,服务器在过滤器中来校验令牌的有效性。如果令牌无效,就响应一个错误的信息,如果令牌有效,就放行去执行相应的操作。

具体流程如下:

  

代码如下:

/*** 令牌校验过滤器*/
@Slf4j
@WebFilter(urlPatterns = "/*")
public class TokenFilter implements Filter {@Overridepublic void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) resp;//1. 获取请求url。String url = request.getRequestURL().toString();//2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行。if(url.contains("login")){ //登录请求log.info("登录请求 , 直接放行");// 这一句代码指的是放行操作chain.doFilter(request, response);return;}//3. 获取请求头中的令牌(token)。String jwt = request.getHeader("token");//4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)。if(!StringUtils.hasLength(jwt)){ //jwt为空log.info("获取到jwt令牌为空, 返回错误结果");// HttpStatus.SC_UNAUTHORIZED指的就是401响应码response.setStatus(HttpStatus.SC_UNAUTHORIZED);return;}//5. 解析token,如果解析失败,返回错误结果(未登录)。try {JwtUtils.parseJWT(jwt);} catch (Exception e) {e.printStackTrace();log.info("解析令牌失败, 返回错误结果");response.setStatus(HttpStatus.SC_UNAUTHORIZED);return;}//6. 放行。log.info("令牌合法, 放行");chain.doFilter(request , response);}}

3.3 Filter详解

过滤器的执行流程如下:

过滤器当中我们拦截到了请求之后,如果希望继续访问后面的web资源,就要执行放行操作,放行就是调用 FilterChain对象当中的doFilter()方法,在调用doFilter()这个方法之前所编写的代码属于放行之前的逻辑。        

在放行后访问完 web 资源之后还会回到过滤器当中,回到过滤器之后如有需求还可以执行放行之后的逻辑,放行之后的逻辑我们写在doFilter()这行代码之后。如果不再访问其他的资源,就会直接执行放行后的逻辑。

拦截路径:

执行流程我们搞清楚之后,接下来再来介绍一下过滤器的拦截路径,Filter可以根据需求,配置不同的拦截资源路径,就是配置@WebFilter 注解中的属性值:

过滤器链:

在一个Web程序中,我们可以设置多个过滤器,这些过滤器就形成了一个过滤器链。过滤器链的执行顺序如图所示:

过滤器链上过滤器的执行顺序:注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序。 比如:

  • AbcFilter

  • DemoFilter

这两个过滤器来说,AbcFilter 会先执行,DemoFilter会后执行。

4. 拦截器interceptor

4.1 令牌校验interceptor

拦截器是Spring框架提供的,类似于过滤器。也是检验用户请求是否携带令牌,若令牌有效,则放行。

过滤器Filter只需要定义一个类实现Filter接口即可,而拦截器interceptor需要分两步:

  1. 定义拦截器

  2. 注册配置拦截器

第一步是定义拦截器,我们需要定义一个类去实现HandlerInterceptor接口,然后去重写preHandle方法(其中的逻辑和Filter中的逻辑是相同的):

@Slf4j
@Component
public class TokenInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1. 获取请求url。String url = request.getRequestURL().toString();//2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行。if(url.contains("login")){ //登录请求log.info("登录请求 , 直接放行");return true;}//3. 获取请求头中的令牌(token)。String jwt = request.getHeader("token");//4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)。if(!StringUtils.hasLength(jwt)){ //jwt为空log.info("获取到jwt令牌为空, 返回错误结果");response.setStatus(HttpStatus.SC_UNAUTHORIZED);return false;}//5. 解析token,如果解析失败,返回错误结果(未登录)。try {JwtUtils.parseJWT(jwt);} catch (Exception e) {e.printStackTrace();log.info("解析令牌失败, 返回错误结果");response.setStatus(HttpStatus.SC_UNAUTHORIZED);return false;}//6. 放行。log.info("令牌合法, 放行");return true;}}

第二步是对拦截器进行配置,定义一个配置类去实现WebMvcConfigurer接口:

// Configuration注解表示当前是一个配置类
@Configuration  
public class WebConfig implements WebMvcConfigurer {// 拦截器对象// 自己定义的拦截器对象@Autowiredprivate TokenInterceptor tokenInterceptor;// 重写的WebMvcConfigurer中的方法@Overridepublic void addInterceptors(InterceptorRegistry registry) {//注册自定义拦截器对象//将对象传入,后面的/**表示对所有路径的请求都进行拦截registry.addInterceptor(tokenInterceptor).addPathPatterns("/**");}
}

4.2 拦截路径

在注册配置拦截器的时候,我们要指定拦截器的拦截路径,通过addPathPatterns("要拦截路径")方法,就可以指定要拦截哪些资源。

/**表示拦截所有资源而在配置拦截器时,不仅可以指定要拦截哪些资源,还可以指定不拦截哪些资源,只需要调用excludePathPatterns("不拦截路径")方法,指定哪些资源不需要拦截。

@Configuration  
public class WebConfig implements WebMvcConfigurer {//拦截器对象@Autowiredprivate DemoInterceptor demoInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//注册自定义拦截器对象registry.addInterceptor(demoInterceptor).addPathPatterns("/**")//设置拦截器拦截的请求路径( /** 表示拦截所有请求).excludePathPatterns("/login");//设置不拦截的请求路径}
}

拦截路径设置如下:

4.3 执行流程

当我们在项目中同时定义过滤器Filter和拦截器interceptor时,请求会先被过滤器进行拦截,然后进入spring环境当中被拦截器拦截。请求执行完之后才会执行postHandle及其以后的逻辑。

5. 拦截器与过滤器的区别

它们之间的区别主要是两点:

  • 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。

  • 拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor属于Spring MVC组件,基于AOP实现,仅拦截Controller请求,即Spring环境中的请求。

http://www.xdnf.cn/news/721405.html

相关文章:

  • python的高级2——函数作为对象
  • ⚽【足球数据全维度解析】从基础统计到高阶分析,数据如何重塑现代足球?
  • 中国国运新引擎:下一代液晶技术突破如何重塑全球显示格局
  • 通过粘性布局实现表格且带有固定列
  • 文件夹的命名与分类
  • Geoserver修行记--点击geoserver服务的WMTS能力(GetCapabilities)文档显示400 null
  • 第五十九节:性能优化-GPU加速 (CUDA 模块)
  • 2025-5-27Vue3快速上手
  • 软考-系统架构设计师-第八章 数据库设计基础知识
  • Lesson 25 Do the English speak English
  • DMBOK对比知识点对比(1)
  • 中国头盔护具展在杭州举办合适
  • 操作系统 Windows Linux macOS如何查看Ollama的存储位置
  • IP地址交换如何让车联网效率翻倍?
  • Jupyter MCP服务器部署实战:AI模型与Python环境无缝集成教程
  • 利用Python直接生成html注意事项
  • 从“无差别降噪”到“精准语音保留”:非因果优化技术为助听设备和耳机降噪注入新活力
  • SAR ADC 比较器噪声分析(一)
  • sensevoice sherpa-onnx部署
  • 嵌入式学习笔记 - freeRTOS任务优先级抢占,时间片抢占的实现机制
  • shell脚本总结12:自定义函数
  • 【Linux 基础知识系列】第一篇-Linux 简介与历史
  • 每天掌握一个Linux命令 - pidstat
  • 《仿盒马》app开发技术分享-- 订单详情页(端云一体)
  • 【数据集】中国江北气候区100m逐日近地表气温数据(Python实现代码解析)
  • 【计网】静态路由分配
  • c#跨平台桌面地图-mapsui
  • The 2020 ICPC Asia Yinchuan Regional Programming Contest
  • 跨越太赫兹鸿沟:高通量实时成像的曙光?
  • DataAgent产品经理(数据智能方向)