【Spring】统一功能处理

news/2025/2/27 9:20:53

目录

前言

拦截器

什么是拦截器?

拦截器的使用

自定义拦截器

注册并配置拦截器

拦截器详解

拦截路径

 拦截器执行流程

适配器模式

统一数据返回格式

优点

统一异常处理


前言

在前面中,我们已经学习了spring中的一些常用操作,那么本篇,我们就继续往下深入学习。

我们在做一些小项目的时候,假如我们想要判断用户是否已经登录,按照我们前面的学习,我们就需要用到seesion来进行判断。设想一下,我们有几个界面,而这几个界面都需要在用户登录后才能进行查看的,就想淘宝,如果我们未登录,那么他就跳转到登录界面。

 对于这样的操作,我们的界面不止一个,那么我们对应的在每个页面调用后端的API,其中的方法每次都需要判断用户是否登录,这样会让代码冗余,所以,在Spring中,给我们提供了一种功能,能够让我们将这些重复的代码进行抽取——拦截器。

拦截器

什么是拦截器?

拦截器(Interceptor)是一种在请求处理流程中,对请求和响应进行拦截和预处理的机制。它允许开发者在请求到达目标处理器(如控制器方法)之前或之后执行统一的逻辑,从而实现诸如权限校验、日志记录、性能监控、请求过滤等功能。

拦截器的使用

拦截器的使用步骤分为两步:

  1. 定义拦截器
  2. 注册并配置拦截器 

自定义拦截器

在Spring  MVC框架中,拦截器通过实现 HandlerInterceptor  接口来定义拦截逻辑

java">package com.example.demo.interceptor;

import com.example.demo.Result.Results;
import com.example.demo.constant.Constants;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

/**
 * 登录拦截器,用于在请求处理前验证用户是否已登录
 */
@Slf4j
@Component
public class LoginInterceptor  implements HandlerInterceptor {

    @Autowired
    private ObjectMapper objectMapper;
    
    /**
     * 在请求处理之前进行拦截
     * 
     * @param request  HttpServletRequest对象,用于获取请求信息
     * @param response HttpServletResponse对象,用于设置响应信息
     * @param handler  请求处理器,可以是HandlerMethod或RequestMappingHandler等
     * @return boolean 表示是否继续执行其他拦截器和目标方法。返回true表示继续执行,返回false表示中断执行。
     * @throws Exception 抛出异常表示拦截器处理出现错误
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("方法执行前");
        //进行登录校验
        HttpSession session= request.getSession(false);
        if(session!=null&&"true".equals(session.getAttribute(Constants.USER_SESSION_KEY))){
            // 用户已登录,继续执行请求
            return true;
        }
        // 用户未登录,返回未授权错误信息
        Results results=Results.unLogin();
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.getOutputStream().write(objectMapper.writeValueAsString(results).getBytes());
        response.setContentType("application/json;charset=UTF-8");
        response.getOutputStream().close();
        return false;
    }

    /**
     * 在请求处理之后,视图渲染之前进行拦截
     * 
     * @param request  HttpServletRequest对象,用于获取请求信息
     * @param response HttpServletResponse对象,用于设置响应信息
     * @param handler  请求处理器,可以是HandlerMethod或RequestMappingHandler等
     * @param modelAndView ModelAndView对象,用于添加模型数据或修改视图
     * @throws Exception 抛出异常表示拦截器处理出现错误
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("方法执行后");
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    /**
     * 在请求完成之后执行的方法
     * 
     * @param request 传入的HTTP请求对象,包含请求相关数据
     * @param response 传入的HTTP响应对象,包含响应相关数据
     * @param handler 处理请求的处理器对象,可以是任何类型的对象
     * @param ex 请求处理过程中发生的异常,如果没有异常,则为null
     * @throws Exception 根据具体实现可能会抛出的异常
     * 
     * 此方法主要用于在请求处理完成后进行一些清理工作,例如关闭数据库连接、释放资源等
     * 它是在请求处理的最后一步调用的,确保了所有处理逻辑已经执行完毕
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}
  • preHandler():该方法是在目标方法执行前执行的。若返回true,则继续执行后续的业务逻辑,返回false,则中断后续的操作。
  • postHandler():该方法是在目标方法执行后再执行的。
  • afterCompletion():该方法是在视图渲染之后执行的,在postHandler()方法之后执行,但由于现在前后端分离,所以后端基本上接触不到视图的渲染,这个方法用的少。

注册并配置拦截器

注册配置拦截器我们需要实现 WebMvcConfiguer 接口,并实现其中的 addInterceptors 方法。

java">package com.example.demo.config;

import com.example.demo.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 配置类用于配置Web相关的设置
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    /**
     * 登录拦截器,用于拦截请求以进行登录验证
     */
    @Autowired
    private LoginInterceptor loginInterceptor;

    /**
     * 添加拦截器以配置请求的预处理和后处理
     * 
     * @param registry 拦截器注册对象,用于注册自定义拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册拦截器
        registry.addInterceptor(loginInterceptor)
                //拦截以"/book/"开头的所有请求
                .addPathPatterns("/book/**")
                //排除"/user/login"请求,使其不被拦截
                .excludePathPatterns("/user/login");
    }
}

假如此时我们未登录,调用一下查询功能:

可以看到,会对请求进行拦截。

 知道了拦截器的是如何使用的,那么就来进一步了解拦截器。

拦截器详解

拦截器的使用细节我们讲以下两个部分:

  • 拦截路径配置
  • 拦截器实现原理

拦截路径

拦截路径指的是我们定义的拦截器对哪些请求生效,我们在注册配置拦截器的时候,通过 addPathPatterns() 方法就可以来指定要拦截哪些请求,也可以通过 excludePathPatterns() 方法来指定哪些请求不需要拦截

关于拦截路径设置的规则,有以下几种:

拦截路径含义举例

/*

一级路径

能匹配/user,/book,但不能匹配/book/getList等

/**任意级路径能匹配/user,/user/login,即任意路径都可以匹配
/book/*/book下的一级路径能匹配/book/addBook,不能匹配/book/addBook/get,/book
/book/**/book下的任意级路径能匹配/book,/book/addBook,/book/addBook/2,不能匹配/user/login

上面的这些拦截规则可以拦截项目中的URL,包括静态文件(如图片文件、JS和CSS等文件).

如果我们使用下面这种拦截规则,就会将前端界面的请求也给拦截住。 

 我们可以通过设置前端界面不拦截:

java">package com.example.demo.config;

import com.example.demo.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 配置类用于配置Web相关的设置
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    /**
     * 登录拦截器,用于拦截请求以进行登录验证
     */
    @Autowired
    private LoginInterceptor loginInterceptor;
    
    public List<String> excludePath= Arrays.asList("/user/*",
            "/css/**",
            "/js/**",
            "/img/**",
            "/**/*.html");//放行路径

    /**
     * 添加拦截器以配置请求的预处理和后处理
     *
     * @param registry 拦截器注册对象,用于注册自定义拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册拦截器
        registry.addInterceptor(loginInterceptor)
                //拦截以"/book/"开头的所有请求
                .addPathPatterns("/**")
                //排除"/user/login"请求,使其不被拦截
                .excludePathPatterns(excludePath);
    }
}

这样,就可以获取到前端界面。

 拦截器执行流程

在没有添加拦截器之前,我们调用顺序是:

在添加拦截器后:

  1. 在添加完拦截器后,执行Controller中的方法之前,请求会先被拦截器拦截住,执行preHandler() 方法,这个方法会返回一个布尔类型的值。如果返回true,说明要放行,继续访问controller中的方法;如果返回false,则不会放行(controller中的方法不会执行).
  2. controller中的方法执行完后,会继续执行 postHandler() 方法以及 afterCompletion() 方法,执行完毕后,最终给浏览器响应数据。

我们通过观察日志,可以看到当Tomcat在启动之后,会有核心的类 DispatcherServlet 来控制程序的执行顺序。

所有的请求都会先进入到DispatcherServlet 中,执行 doDispatch() 调度方法,如果有拦截器,就会先执行拦截器中 preHandle() 方法中的代码,如果 preHandle() 返回true,那么就会继续访问controller中的方法,当controller中的方法执行完毕,就会再回过来执行 postHandle() 和 afterCpmpletion() 方法,返回给DispatcherServlet,最终给浏览器响应数据。

 我们观察 DispatcherServlet 中的源码,就可以看到这三个方法的执行流程:

DispatcherServlet  源码中,我还可以看到使用了适配器模式

适配器模式

 适配器模式也叫包装器模式。将一个类的接口,转换成客户期望的另有一个接口,适配器让原本接口不兼容的类可以合作无间。

简单来说:就是目标类不能直接使用,通过一个新类进行包装,适配调用方使用,把两个不兼容的接口通过一定的方式使之兼容

 使用适配器来使两个接口兼容:

其实在我们前面学习Spring日志的时候,其中的 slf4j 就使用到了适配器模式,我们想要打印日志的时候,只需要调用slf4j的API,而它会自动去调用底层的 log4j 或者 logback 来打印日志。

示例:

java">package com.example.demo.config;

/**
 * Slf4j接口定义了打印日志的方法
 */
public interface Slf4j {
    /**
     * 打印日志信息
     * @param message 需要打印的日志信息
     */
    void print(String message);
}

/**
 * Log4j类提供了具体的日志打印实现
 */
class Log4j{
    /**
     * 打印日志信息
     * @param message 需要打印的日志信息
     */
    void log4jPrint(String message){
        System.out.println("Log4j: "+message);
    }
}

/**
 * Slf4jAdapter类是Slf4j接口与Log4j类之间的适配器
 * 它使得Log4j可以通过Slf4j接口来打印日志
 */
class Slf4jAdapter  implements Slf4j{
    
    /**
     * log4j实例用于实际的日志打印
     */
    private Log4j log4j;
    
    /**
     * 构造函数,接收一个Log4j实例
     * @param log4j Log4j实例,用于实际的日志打印
     */
    public Slf4jAdapter(Log4j log4j) {
        this.log4j = log4j;
    }
    
    /**
     * 实现Slf4j接口的print方法,通过Log4j实例来打印日志
     * @param message 需要打印的日志信息
     */
    @Override
    public void print(String message) {
        log4j.log4jPrint(message);
    }
}


/**
 * Demo类包含主程序,用于演示Slf4jAdapter的使用
 */
class Demo{
    /**
     * 主函数,创建Log4j实例并通过Slf4jAdapter适配,然后打印日志信息
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        Log4j log4j = new Log4j();
        Slf4j slf4j = new Slf4jAdapter(log4j);
        slf4j.print("Hello World");
    }
}

可以看到,我们不需要修改log4j的api,只需要使用适配器,就可以更换日志框架,维护系统。

那么为什么不直接调用Log4j呢? 

 适配器模式其实可以看做一种“补偿模式”,用来补救设计上的缺陷,使用这种模式是无奈之举。如果在设计初期,能够规避接口不兼容的问题,那么就不需要使用适配器模式了。

统一数据返回格式

我们在做项目的时候,如果前端调用后端返回的数据格式都不同,后序如果修改起来,就显得有点杂乱,所以我们可以对返回的数据格式进行统一——统一数据返回格式

在SpringBoot,如果我们想要在返回数据响应之前对数据进行一些逻辑操作,那么我们就需要使用到注解 @ControllerAdvice ResponseBodyAdvice 接口的实现。

定义下存常量的类:

java">package com.example.demo.constant;

public class Constants {
    public static final String USER_SESSION_KEY = "user";
    public static final Integer SUCCESS_CODE = 200;//成功
    public static final Integer FAIL_CODE = -2;//失败
    public static final Integer UNLOGIN_CODE = -1;//未登录

}

统一返回数据格式:

java">package com.example.demo.Result;


import com.example.demo.constant.Constants;

import lombok.Data;

@Data
public class Results<T> {
    private Integer code;//200成功,-1未登录,-2程序异常
    private String msg;
    private  T data;

    public static <T> Results success(T data){
        Results results=new Results();
        results.setCode(Constants.SUCCESS_CODE);
        results.setMsg("");
        results.setData(data);
        return results;
    }

    public static  Results unLogin(){
        Results results=new Results();
        results.setCode(Constants.UNLOGIN_CODE);
        results.setMsg("用户未登录");
        return results;
    }

    public static <T> Results fail(String msg){
        Results results=new Results();
        results.setCode(Constants.FAIL_CODE);
        results.setMsg(msg);
        return results;
    }

    public static <T> Results fail(){
        Results results=new Results();
        results.setCode(Constants.FAIL_CODE);
        results.setMsg("程序出现异常");
        return results;
    }
}

响应处理:

java">package com.example.demo.config;

import com.example.demo.Result.Results;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

/**
 * 全局响应处理,统一处理所有响应
 * 该类实现了ResponseBodyAdvice接口,用于自定义响应体
 */
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 判断是否支持当前的返回类型和转换器类型
     * 该方法始终返回true,表示支持所有类型的响应处理
     */
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    /**
     * 在写入响应体前处理数据
     * 该方法根据返回的数据类型,进行相应的处理和封装
     * 如果返回类型是String,则将其作为成功消息封装进Results对象
     * 如果返回类型已经是Results,则直接返回
     * 否则,将返回值作为成功数据封装进Results对象
     */
    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if(body instanceof String){
            return  objectMapper.writeValueAsString(Results.success(body));
        }
        if(body instanceof Results){
            return body;
        }
        return Results.success(body);
    }
}

调用一下看看:

可以看到,这样的话,如果前端想要获取到我们后端的数据,以及后续修改操作等,就更加清晰容易操作了。

我们再来看一处地方:

这里为什么要这样写呢?

 SpringMVC默认会注册一些自导的HttpMessageConverter(从先后顺序排序分别为ByteArrayHttpMessageConverter、StringHttpMessageConverter、SourceHttpMessageConverte、SourceHttpMessageConverterr、AllEncompassingFormHttpMessageConverter)

而其中的 AllEncompassingFormHttpMessageConverter  会根据项目依赖情况添加对应的HttpMessageConverter。

在依赖中引⼊jackson包后,容器会把 MappingJackson2HttpMessageConverter ⾃动注册到
messageConverters 链的末尾。Spring会根据返回的数据类型,从 messageConverters 链选择合适的 HttpMessageConverter。当返回的数据是⾮字符串时,使⽤的MappingJackson2HttpMessageConverter 写⼊返回对象当返回的数据是字符串时, StringHttpMessageConverter 会先被遍历到,这时会认为
StringHttpMessageConverter 可以使用

可以看到子类StringHttpMessageConverter中的addDefaultHeaders()方法接收的参数为String,但我们需要返回的类型为Results类型,所以这里我们就需要利用SpringBoot内置提供的Jackson来实现信息的序列化。即:

  • @SneakyThrows 是 Lombok 提供的一个注解,用于简化 Java 中的异常处理。它允许开发者在方法中抛出受检异常(checked exceptions),而无需在方法签名中显式声明 throws,也不需要使用 try-catch 块。

优点

  • ⽅便前端程序员更好的接收和解析后端数据接口返回的数据
  • 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就可以了,因为所有接口都是这样返回的
  • 有利于项目统⼀数据的维护和修改
  • 有利于后端技术部⻔的统⼀规范的标准制定,不会出现稀奇古怪的返回内容

统一异常处理

 当我们的程序出现异常时,我们也可以对这些异常进行统一处理。

统一异常处理使用的是 @ControllerAdvice@ExceptionHandler 来实现的。

  • @ControllerAdvice 表⽰控制器通知类
  • @ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执⾏某个⽅法事件
java">package com.example.demo.config;

import cn.hutool.core.io.resource.NoResourceException;
import com.example.demo.Result.Results;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.resource.NoResourceFoundException;

/**
 * 全局异常处理类
 * 用于统一处理项目中的异常,提高代码的健壮性和用户体验
 */
@ControllerAdvice
@Slf4j
@ResponseBody
public class ErrorAdviceHandler {

    /**
     * 处理通用异常
     * @param e 异常对象
     * @return 返回处理结果
     */
    @ExceptionHandler
    public Object handler(Exception e){
        log.error("异常信息:{}",e.getMessage());
        return Results.fail(e.getMessage());
    }

    /**
     * 处理数组越界异常
     * @param e 异常对象
     * @return 返回处理结果
     */
    @ExceptionHandler
    public Object handler(ArrayIndexOutOfBoundsException e){
        log.error("异常信息:{}",e.getMessage());
        return Results.fail(e.getMessage());
    }

    /**
     * 处理空指针异常
     * @param e 异常对象
     * @return 返回处理结果
     */
    @ResponseStatus
    @ExceptionHandler
    public Object handler(NullPointerException e){
        log.error("异常信息:{}",e.getMessage());
        return Results.fail(e.getMessage());
    }

    /**
     * 处理算数异常
     * @param e 异常对象
     * @return 返回处理结果
     */
    @ExceptionHandler
    public Object handler(ArithmeticException e){
        log.error("异常信息:{}",e.getMessage());
        return Results.fail(e.getMessage());
    }

    /**
     * 处理资源未找到异常
     * @param e 异常对象
     * @return 返回处理结果
     */
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler
    public Object handler(NoResourceFoundException e){
        log.error("异常信息:{}  path:{}",e.getDetailMessageCode(),e.getResourcePath());
        return Results.fail(e.getMessage());
    }
}

 测试案例:

java">package com.example.demo.Controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * Demo控制器类,用于处理演示项目的HTTP请求
 */
@RestController
@RequestMapping("/demo")
public class DemoController {
    
    /**
     * 处理/t1请求的方法
     * 该方法演示了未处理的除零异常,用于教学或测试目的
     * 
     * @return 成功信息字符串,但在执行中将引发除零异常
     */
    @RequestMapping("/t1")
    public String t1(){
        // 该行代码将产生除零异常
        int a=1/0;
        return "success";
    }
    
    /**
     * 处理/t2请求的方法
     * 该方法演示了未处理的空指针异常,用于教学或测试目的
     * 
     * @return 成功信息字符串,但在执行中将引发空指针异常
     */
    @RequestMapping("/t2")
    public String t2(){
        // 该行代码将产生空指针异常
        String str=null;
        str.length();
        return "success";
    }
    
    /**
     * 处理/t3请求的方法
     * 该方法演示了未处理的数组越界异常,用于教学或测试目的
     * 
     * @return 成功信息字符串,但在执行中将引发数组越界异常
     */
    @RequestMapping("/t3")
    public String t3(){
        // 初始化一个包含三个元素的数组
        int[] arr={1,2,3};
        // 该行代码将产生数组越界异常
        arr[3]=4;
        return "success";
    }
}

此外,如果发生空指针异常或者其他异常,异常处理中有Exception和NullPointerException的话,优先执行具体的异常,即执行NullPointerException异常。

具体异常优先于通用异常


以上就是本篇所有内容~

若有不足,欢迎指正~ 


http://www.niftyadmin.cn/n/5869865.html

相关文章

Qt QScrollArea 总结

Qt QScrollArea 总结 1. 功能概述 滚动容器&#xff1a;用于显示超出视口&#xff08;Viewport&#xff09;范围的内容&#xff0c;自动提供滚动条。子部件管理&#xff1a;可包裹单个子部件&#xff08;通过 setWidget()&#xff09;&#xff0c;当子部件尺寸 > 视口时&a…

ARM32汇编 -- align 指令说明及示例

.align 指令说明及示例 .align 指令的作用 .align 是 ARM 汇编中的伪指令&#xff0c;用于将接下来的代码或数据对齐到特定的地址边界。对齐操作可以提高程序的执行效率&#xff0c;确保指令或数据存储在符合处理器要求的地址上。 .align 的语法 .align nn 是一个整数&…

CineMaster: 用于电影文本到视频生成的 3D 感知且可控的框架。

CineMaster是一种 3D 感知且可控的文本到视频生成方法允许用户在 3D 空间中联合操纵物体和相机&#xff0c;以创作高质量的电影视频。 相关链接 论文&#xff1a;cinemaster-dev.github.io 论文介绍 CineMaster是一种用于 3D 感知和可控文本到视频生成的新型框架。目标是让用…

深圳南柯电子|医疗设备EMC测试整改检测:零到一,保障医疗安全

在当今医疗科技飞速发展的时代&#xff0c;医疗设备的电磁兼容性&#xff08;EMC&#xff09;已成为确保其安全、有效运行的关键要素之一。EMC测试整改检测不仅关乎设备的性能稳定性&#xff0c;更是保障患者安全、避免电磁干扰引发医疗事故的重要措施。 一、医疗设备EMC测试整…

ZIP64扩展和普通ZIP文件有什么区别?

ZIP64扩展是ZIP文件格式的一个扩展&#xff0c;旨在解决传统ZIP格式的限制&#xff0c;尤其是文件大小和数量的限制。以下是ZIP64扩展与普通ZIP文件的主要区别&#xff1a; 1. 文件大小限制 普通ZIP文件&#xff1a; 单个文件大小限制为 4GB&#xff08;2^32字节&#xff09;。…

【综合项目】api系统——基于Node.js、express、mysql等技术

目录 0 前言 1 初始化 2 注册登录 2.1 注册 2.1.1 功能&#xff1a;密码加密&#xff08;2.3.3&#xff09; 2.1.1.1 操作 2.1.1.2 bcryptjs详解 2.1.2 优化&#xff1a;表单数据验证&#xff08;2.5&#xff09; 2.1.2.1 过时代码修正 2.1.2.2 关键操作 0 前言 …

STM32--SPI通信讲解

前言 嘿&#xff0c;小伙伴们&#xff01;今天咱们来聊聊STM32的SPI通信。SPI&#xff08;Serial Peripheral Interface&#xff09;是一种超常用的串行通信协议&#xff0c;特别适合微控制器和各种外设&#xff08;比如传感器、存储器、显示屏&#xff09;之间的通信。如果你…

Web开发:ORM框架之使用Freesql的导航属性

一、什么时候用导航属性 看数据库表的对应关系&#xff0c;一对多的时候用比较好&#xff0c;不用多写一个联表实体&#xff0c;而且查询高效 二、为实体配置导航属性 1.给关系是一的父表实体加上&#xff1a; [FreeSql.DataAnnotations.Navigate(nameof(子表.子表关联字段))]…