深入剖析Tomcat(九) Session的实现原理

news2024/10/5 15:24:10

提到Session,相信大家都不陌生,Http协议本身是无状态的,每次请求都是独立的,而当我们想要将多次请求建立某种关系的时候,就会用到Cookie+Session这个组合,也就是常说的“会话”概念,将多次请求当成一次会话来看待。而Tomcat恰恰就是支持这个机制的。

Cookie+Session

Cookie是由客户端来支持的,这个客户端通常是由浏览器来担任,当使用ip或域名访问一个服务时,如果在服务端的响应头中有“Set-Cookie”这个key,证明服务端给了客户端一些Cookie,在接下来的请求中,客户端就要在请求头中带上这些Cookie。在Tomcat的会话机制中,这个Cookie通常叫做“JSESSIONID”。

Session由服务端来管理,准确的说,是由Tomcat中的一个Context容器持有的Session管理器来管理。一个服务端可能与多个客户端建立会话,所以Session管理器要管理多个Session对象,管理的方式其实就是用一个Map来存储所有活跃的Session,key为Session的id,value就是Session对象本身。而“JSESSIONID”其实就是这个Map的key,所以Tomcat通过JSESSIONID就能找到对应的Session对象。

至于为什么是Context容器持有Session管理器,前面文章有讲过,一个Context容器代表一个Web应用,而我们客户端访问的其实就是一个Web应用的接口,与这个Web应用建立会话。Tomcat本身是支持部署多个Web应用的,也就是可能会有多个Context容器,他们分别管理自己的会话;但是基于目前springboot大流行的场景下,一个Tomcat也就只包含了一个Context容器,针对的也就是我们这个springboot项目。

在Tomcat中,Session只存在于内存中吗?关闭Tomcat后,所有Session就会丢失吗?

答案是不会!默认情况下,Tomcat在正常关闭时,会将当前活跃的Session序列化存储到一个本地文件中,这个文件的文件名默认为“SESSIONS.ser”;Tomcat启动时会去找这个文件,如果文件存在的话,就将这个文件中的Session集合反序列化回来,重新放到内存中。当然如果你觉得你的应用中Session非常重要的话,Tomcat还支持你将Session存储到数据库中(这个好像用到的场景不多)。

如果你kill -9杀进程的话,Tomcat没有正常关闭,是来不及将session持久化的。

每个首次请求的Http请求,Tomcat都会为其创建Session吗?

不都会。Tomcat不会为所有请求都创建Session,仅当某个请求需要Session支持时才创建Session。那什么情况下代表它需要Session支持呢?通常情况下是在servlet中显示调用“HttpServletRequest.getSession()”方法后,就代表它需要Session了。如果一个全新的请求打进来后,没有任何逻辑去获取Session,那Tomcat就不会为其创建Session。

Seesion有效期是怎么刷新的呢?如何判断过没过有效期?

每个Session对象都会维护一个“最后一次访问时间”的字段,叫做“lastAccessedTime” ;另外还维护了一个Session有效期的时长字段,叫做“maxInactiveInterval”。

判断Session有没有过期的算法为“ 当前时间- lastAccessedTime > maxInactiveInterval”,为true就是过期了;基于当前会话,如果后续请求打进来后,Session会自动将lastAccessedTime更新为当前时间,也就是刷新了有效期(对应StandardSession#access()方法,该方法是被一个叫AuthenticatorBase的阀调用的)。

Session过期后,Session管理器会将该对象回收利用,下次需要创建Session对象时,如果回收池中有可用对象的话,就直接用了,避免了重复创建对象。什么时候触发这个Session过期的判断呢?有两个触发点:

  1. 每次请求打进来后,如果携带了sessionId,就判断一次   
  2. Tomcat后台维护一个线程,每隔一段时间就检查一遍所有Session,判断有没有过期。

上面大白话说了一堆,下面来看看Tomcat是怎么设计这套Session管理机制的。

Session设计

Session对象

Session在Tomcat中以对象的形式存在。

这里有个概念要提前弄明白,Tomcat是基于servlet规范来设计的,servlet规范中对于容器、Session等有一套定义好的接口,比如Session类就是实现了javax.servlet.http.HttpSession 接口的类,这些servlet规范的接口都在servlet.jar中。再说Catalina,它是servlet容器的一个实现,可以理解为是以一个插件的形式放在Tomcat中,是存在被替换掉的可能性的。Catalina内部为了实现特有的功能,会自己定义一些接口,比如针对Session就设计了org.apache.catalina.Session接口。所以在Catalina中,一个Session的具体实现类要继承两个接口:来自servlet的HttpSession接口与自己yy的Session接口。但是,当Catalina要把做好的Session对象提供给具体的servlet进行使用时,就不能暴露自己在org.apache.catalina.Session中定义的方法了,因为这个接口是它自己yy的,没在servlet规范中,如果某天Tomcat心血来潮替换掉了Catalina这个组件,那这个yy的接口就根本不存在了,所以只能让servlet使用javax.servlet.http.HttpSession这个接口中定义的方法。 但是catalina中的Session实现类是实现了两个接口的,怎么办呢?Tomcat针对这种事件的通用做法就是使用门面类,类名通常就叫做 xxxxFacade。以静态代理的方式,使用门面类只暴露HttpSession接口中的方法即可,将这个门面类给servlet使用就OK了。

在Tomcat中,每种组件的规范,其实都是通过接口来定义的,比如本次讲的Session就是HttpSession接口与Session接口,接口代码就不往这放了,随便找个Web项目你都能看到,这里我仅以Catalina中对Session的默认实现类StandardSession,来简单讲一下Session对象都有啥

下面是删减后的StandardSession类的代码

public class StandardSession implements HttpSession, Session, Serializable {

    // 该session中的属性集合,通常我们往session中放东西就是放到了这个属性里
    protected ConcurrentMap<String, Object> attributes = new ConcurrentHashMap<>();

    // session id
    protected String id = null;

    // 当前请求的访问时间
    protected volatile long thisAccessedTime = creationTime;

    // 最后一次访问时间
    protected volatile long lastAccessedTime = creationTime;

    // 该session关联的session管理器
    protected transient Manager manager = null;

    // session会话的超时时间,单位为秒,负值代表永不超时
    protected volatile int maxInactiveInterval = -1;

    // 该sesssion会话请求次数计数器
    protected transient AtomicInteger accessCount = null;

    // Session的门面类,提供给servlet使用
    protected transient StandardSessionFacade facade = null;



    /**
     * 返回一个实现了HttpSession接口的门面类的对象,供servlet使用
     */
    @Override
    public HttpSession getSession() {
        if (facade == null) {
            if (SecurityUtil.isPackageProtectionEnabled()) {
                facade = AccessController.doPrivileged(new PrivilegedNewSessionFacade(this));
            } else {
                facade = new StandardSessionFacade(this);
            }
        }
        return facade;
    }

    /**
     * 当该Session相关的请求进来时,更新访问时间。这个方法由Context容器自动调用,无需用户手动在servlet中调用
     */
    @Override
    public void access() {

        this.thisAccessedTime = System.currentTimeMillis();

        if (ACTIVITY_CHECK) {
            accessCount.incrementAndGet();
        }
    }

    /**
     * 当请求访问结束时(已经执行了相关servlet的方法了),调用此方法,更新lastAccessedTime的值
     */
    @Override
    public void endAccess() {

        isNew = false;

        /**
         * The servlet spec mandates to ignore request handling time
         * in lastAccessedTime.
         */
        if (LAST_ACCESS_AT_START) {
            this.lastAccessedTime = this.thisAccessedTime;
            this.thisAccessedTime = System.currentTimeMillis();
        } else {
            this.thisAccessedTime = System.currentTimeMillis();
            this.lastAccessedTime = this.thisAccessedTime;
        }

        if (ACTIVITY_CHECK) {
            accessCount.decrementAndGet();
        }
    }

    /**
     * 当判断出session过期后,调用此方法使session过期
     */
    @Override
    public void expire() {
        expire(true);
    }

    /**
     * 使session失效
     *
     * @param notify 是否通知监听器
     */
    public void expire(boolean notify) {

        if (notify) {
            // 通知监听器
        }

        // session管理器移除该session

        // 该将session置为无效

        // 解绑此session关联的属性对象,即attributes中的值。无用的对象就可以被GC掉了
    }

    /**
     * 回收该session。
     * 释放所有的对象引用,初始化实例变量的值,准备被再次使用
     */
    @Override
    public void recycle() {

        // Reset the instance variables associated with this Session
        attributes.clear();
        setAuthType(null);
        creationTime = 0L;
        expiring = false;
        id = null;
        lastAccessedTime = 0L;
        maxInactiveInterval = -1;
        notes.clear();
        setPrincipal(null);
        isNew = false;
        isValid = false;
        manager = null;
    }

}

我们最常用到的应该就是attributes这个属性了,通常我们会在会话中放入一些信息,如验证码、用户信息等,就是放到了这个属性中。类似于下面这种用法

    @GetMapping("/login")
    public String login(HttpServletRequest request) {
        //  登录验证的逻辑……

        HttpSession session = request.getSession();
        session.setAttribute("userInfo",user);
        
        // 其他逻辑……
    }

其他各项属性都是为了控制Session而存在的,平时用时感知不大。如session有效期的控制,提供门面装饰对象StandardSessionFacade等,它还提供了recycle方法来重置自己,方便被回收利用。

装饰类StandardSessionFacade就比较简单了,它只实现了HttpSession接口,内部代理一个HttpSession对象(通常来说就是StandardSession对象),接口的实现逻辑都是直接调用的代理对象的同名方法。

public class StandardSessionFacade implements HttpSession {

    public StandardSessionFacade(HttpSession session) {
        this.session = session;
    }

    private final HttpSession session;

    @Override
    public long getCreationTime() {
        return session.getCreationTime();
    }

    @Override
    public String getId() {
        return session.getId();
    }

    // 其他方法省略 …………
}

上面提到的相关类的类图如下

Session管理器

上面展示了Session对象,它代表一个会话,这个对象里存放了该会话的一些属性。Session管理器就是管理多个Session对象的一个存在,catalina制定了Session管理器的规范,封装成了org.apache.catalina.Manager接口,并提供了ManagerBase类(实现了Manager接口)来提供Session管理器的一些统一行为。StandardManager是ManagerBase的子类,是Tomcat默认的Session管理器。下面我综合ManagerBase与StandardManager中的一些属性和方法,来展示下Session管理器的主要作用

首先看这两个属性

// 当前活跃的session,key为session的id
protected Map<String, Session> sessions = new ConcurrentHashMap<>();

// 已经被回收的Session集合,可以被再次使用(这个回收利用的机制好似高版本已经不用了,读者注意版本区别,这里是Tomcat4的逻辑)
protected List<Session> recycled = new ArrayList<>();

Session管理器负责创建Session,对应的方法为createSession,HttpServletRequest.getSession()最终也会调到这个createSession方法。createSession中会尝试利用回收池中的Session,减少对象的创建,节省资源。

public Session createSession() {

    // Recycle or create a Session instance
    Session session = null;
    synchronized (recycled) {
        int size = recycled.size();
        if (size > 0) {
            session = recycled.get(size - 1);
            recycled.remove(size - 1);
        }
    }
    if (session != null)
        session.setManager(this);
    else
        session = new StandardSession(this);

    // 为新的Session初始化属性值
    session.setNew(true);
    session.setValid(true);
    session.setCreationTime(System.currentTimeMillis());
    session.setMaxInactiveInterval(this.maxInactiveInterval);
    String sessionId = generateSessionId();
    String jvmRoute = getJvmRoute();
    if (jvmRoute != null) {
        sessionId += '.' + jvmRoute;
        session.setId(sessionId);
    }
    // setId这个方法中会将这个session加入到sessions这个Map中
    session.setId(sessionId);

    return session;
}

Session管理器支持通过sessionId来查找session,其实就是对sessions这个Map做检索, 方法太简单,不再放代码了。

Session管理器支持序列化sessions到“SESSIONS.ser”文件中,也支持从“SESSIONS.ser”文件中反序列化出Session对象集合,放到sessions属性中。对应的方法分别为load()和unload(),这两个方法是在对应的生命周期方法执行时被调用的,比如 Session管理器组件初始化时执行的start()方法中会调用load()方法,组件销毁时调用的stop()方法中会调用unload()方法。代码不在展示了,感兴趣的同学去看下源码吧。

Session管理器还负责销毁那些已经失效的Session对象,Tomcat中会有一个独立线程周期性的调用StandardManager的processExpires方法来巡检所有Session对象,销毁那些已过期的。

private void processExpires() {

    long timeNow = System.currentTimeMillis();
    Session sessions[] = findSessions();

    for (int i = 0; i < sessions.length; i++) {
        StandardSession session = (StandardSession) sessions[i];
        if (!session.isValid()) {
            continue;
        }
        int maxInactiveInterval = session.getMaxInactiveInterval();
        if (maxInactiveInterval < 0) {
            continue;
        }
        // 计算出session已经空闲的时间,与最大空闲时间做对比,如果超时了,就将session进行过期处理(重置并放入回收池)
        int timeIdle = (int) ((timeNow - session.getLastAccessedTime()) / 1000L);
        if (timeIdle >= maxInactiveInterval) {
            try {
                session.expire();
            } catch (Throwable t) {
                log(sm.getString("standardManager.expireException"), t);
            }
        }
    }

}

除了StandardManager,Tomcat还提供了PersistentManager来支持将Session持久化到不同的储存库中,可以是文件(FileStore)也可以是数据库(JDBCStore),用的比较少,这里就不详细介绍了。

上面提到的类的类图如下

下面画个流程图来串一下 Session建立、刷新、回收的过程

集群共享Session

线上系统为了高可用,一般都会采用集群部署,并使用负载均衡工具(如Nginx)做负载。这会带来一个问题:Tomcat中的Session管理都是针对单机服务而言的,集群服务如何管理Session呢?通常这就需要一个外部介质来存Session了,而且这个外部介质需要是集群中每个节点都能访问到的。通常来说我们会选择redis来充当这个介质,而spring也提供了spring-session-data-redis包来支持这项工作。

spring-session-data-redis的用法

首先引入maven依赖

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
    <version>2.7.4</version>
</dependency>

然后在springboot的启动类上加上@EnableRedisHttpSession注解,就完事了,用起来非常简单。

@EnableRedisHttpSession注解中有几个属性,方便你做一些个性化设置,如果没有特殊需求,用它的默认值即可。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
public @interface EnableRedisHttpSession {

    /**
     * Session的有效期,单位为秒,默认是30分钟
     */
    int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;

    /**
     * 为key定义唯一的命名空间。默认为“spring: session:”,用于隔离会话。
     * 例如,如果你有一个名为“应用A”的应用程序需要与“应用B”保持会话隔离,你可以为应用程序设置两个不同的值,它们可以在同一个Redis实例中运行。
     */
    String redisNamespace() default RedisSessionRepository.DEFAULT_KEY_NAMESPACE;

    /**
     * Redis会话的刷新模式。默认是ON_SAVE,在HTTP响应提交之前将Session刷新到redis。
     * 将该值设置为IMMEDIATE时,对Session有任何更新时,都会立即写入到Redis。
     */
    FlushMode flushMode() default FlushMode.ON_SAVE;

    /**
     * 保存session的模式,默认为ON_SET_ATTRIBUTE,只向redis中刷新发生改变的属性。
     * 设为ON_GET_ATTRIBUTE时,除了session中属性变化时会保存,在从session中读取属性时也会向redis刷新数据。
     * 设为ALWAYS,代表对session的任何操作,都会向redis中刷新数据
     */
    SaveMode saveMode() default SaveMode.ON_SET_ATTRIBUTE;

}

最常使用的属性是maxInactiveIntervalInSeconds,自定义一个session有效期。

其次是redisNamespace,在多个不同的服务都需要往同一个redis实例中放session时,会通过这个属性来做服务区分。

flushMode和saveMode 几乎没用过。甚至我对saveMode这个属性的应用场景都没看懂😂。

spring-session-data-redis的原理

用法很简单,原理才重要。

工作原理
  1. 会话存储替换:Spring Session Data Redis 替换了标准的 HTTP 会话存储机制,通过 Redis 进行会话数据的存储和管理。

  2. 拦截器和过滤器:Spring Session 提供了一个 SessionRepositoryFilter 过滤器,拦截所有进入的 HTTP 请求,替换原有的 HttpSession 实现。

  3. RedisIndexedSessionRepository:该类持有RedisTemplate引用,将会话数据存储在 Redis 中,并负责会话的创建、更新、删除等操作。

主要流程

1.请求拦截SessionRepositoryFilter是一个servlet过滤器,它会拦截所有HTTP 请求,使用自己的requestWrapper与responseWrapper对象将原本的request与response对象包装了一下,然后将包装后的对象扔回了过滤器链中,也就是说当请求到达servlet后,servlet拿到的是这个requestWrapper与responseWrapper对象。 

这个FIlter是设计的精髓,拦截到请求并将request与response偷梁换柱后,自己想特殊实现的逻辑就在xxxWrapper类中重写相关方法,还想使用原逻辑的就还调用原来对象的方法。

2.会话操作与持久化

再看下SessionRepositoryFilter的类结构,在SessionRepositoryRequestWrapper这个类中还有一个HttpSessionWrapper类,这个类就是用来替掉Tomcat原本的HttpSession的。

RedisIndexedSessionRepository类持有一个RedisTemplate对象,负责与redis进行交互。HttpSessionWrapper对session的操作会调用RedisIndexedSessionRepository类中对应方法去redis中存取数据。

redis中存储的session数据

spring-session-data-redis将一个session放入redis时,会创建三个key。Hash结构的key存储的是session的数据,我们编码用到的也是这个数据;另外两个key是与session过期处理相关的,平常我们也不用关心。

前端收到的cookie

使用spring-session-data-redis后,这个框架会将真实的sessionId进行base64编码后再返给前端,如上图的session信息,前端收到的cookie就为  

SESSION=Zjc0MWNmMzYtYTZiMy00YmY2LTgyNDQtM2U3MGYzNmE4NmI0

注意,这时候已经没有“JSESSIONID”了,前后端辨认会话使用“SESSION”。

好,spring-session-data-redis就聊到这里。

下篇文章聊一聊Tomcat的安全性,敬请期待!

源码分享

https://gitee.com/huo-ming-lu/HowTomcatWorks

本篇文章并没有讲原书中的示例,感兴趣的同学可以自己运行观察一下。另外,我在gitee上提供的源码为Tomcat4的代码,写这篇文章时我也参考了Tomcat9的代码,新版和老版的整体思路没变,在一些细节上有些优化,可以先看老版代码,再对比着新版代码去看。

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

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

相关文章

【git】subtree 简单教程

git subtree使用案例 &#x1f604;生命不息&#xff0c;写作不止 &#x1f525; 继续踏上学习之路&#xff0c;学之分享笔记 &#x1f44a; 总有一天我也能像各位大佬一样 &#x1f3c6; 博客首页 怒放吧德德 To记录领地 &#x1f31d;分享学习心得&#xff0c;欢迎指正&am…

ceph radosgw 原有zone placement信息丢失数据恢复

概述 近期遇到一个故障环境&#xff0c;因为某些原因&#xff0c;导致集群原有zone、zonegroup等信息丢失&#xff08;osd&#xff0c;pool等状态均健康&#xff09;。原有桶和数据无法访问&#xff0c;经过一些列fix后修复&#xff0c; 记录过程 恢复realm和pool相关信息 重…

Golang TCP网络编程

文章目录 网络编程介绍TCP网络编程服务器监听客户端连接服务器服务端获取连接向连接中写入数据从连接中读取数据关闭连接/监听器 简易的TCP回声服务器效果展示服务端处理逻辑客户端处理逻辑 网络编程介绍 网络编程介绍 网络编程是指通过计算机网络实现程序间通信的一种编程技术…

SLC Flash SD芯片:高性能存储的优选

SLC Flash SD芯片是一种采用单阶存储单元&#xff08;SingleLevel Cell&#xff0c;SLC&#xff09;技术的Secure Digital&#xff08;SD&#xff09;存储卡。SLC技术以其快速的传输速度、低功耗和较长的存储单元寿命而闻名。 MK米客方德 SLC Flash的优势 1. 快速的传输速度&a…

蓝牙AOA基站定位的优势与应用前景

在科技飞速发展的今天&#xff0c;人们对于定位技术的精度和稳定性要求越来越高。蓝牙AOA基站定位技术应运而生&#xff0c;以其高精度和低通信开销的特点&#xff0c;正逐渐改变我们对室内定位的认知。本文我们就一起来具体了解一下关于蓝牙AOA基站定位技术的优势及应用前景&a…

实现飞书机器人推送消息到指定群组或者用户

实现飞书机器人推送消息到指定群组或者用户 1 简介2 创建飞书应用2.1 注册登录2.2 创建应用2.3 添加应用能力2.4 权限管理3 发布应用4 代码示例4.1 获取应用ID与token4.2 使用Python SDK4.3 简单示例4.4 获取用户或机器人所在的群列表4.5 通过手机号或邮箱获取用户 ID4.6 给群组…

【Vue】普通组件的注册使用-局部注册

文章目录 一、组件注册的两种方式二、使用步骤三、练习 一、组件注册的两种方式 局部注册&#xff1a;只能在注册的组件内使用 ① 创建 .vue 文件 (三个组成部分) 以.vue结尾的组件&#xff0c;一般也叫做 单文件组件&#xff0c;即一个组件就是组件里的全部内容 ② 在使用的组…

【栈】1096. 花括号展开 II

本文涉及知识点 栈 LeetCode 1096. 花括号展开 II 如果你熟悉 Shell 编程&#xff0c;那么一定了解过花括号展开&#xff0c;它可以用来生成任意字符串。 花括号展开的表达式可以看作一个由 花括号、逗号 和 小写英文字母 组成的字符串&#xff0c;定义下面几条语法规则&…

树莓派4B_OpenCv学习笔记4:测试摄像头_imread加载显示图像_imwrite保存图片

今日继续学习树莓派4B 4G&#xff1a;&#xff08;Raspberry Pi&#xff0c;简称RPi或RasPi&#xff09; 本人所用树莓派4B 装载的系统与版本如下: 版本可用命令 (lsb_release -a) 查询: Opencv 版本是4.5.1&#xff1a; 今日对之前的测试CSI摄像头函数进行一些理解说明&#x…

混合关键性系统技术【同构异构】【SMP、AMP、BMP】【嵌入式虚拟化】

混合关键性系统技术【同构异构】【SMP、AMP、BMP】【嵌入式虚拟化】 1 介绍1.1 概述openEuler Embedded 的运行模式openEuler Embedded 混合关键性系统技术架构UniProton 1.2 同构异构区别 【硬件侧】1.3 系统架构【SMP、AMP、BMP】多核处理器平台的系统架构 【软件侧】【SMP、…

【RuoYi】框架中使用wangdietor富文本编辑器

一、前言 在上篇博客中&#xff0c;介绍了RuoYi中如何实现文件的上传与下载&#xff0c;那么这篇博客讲一下如何在RuoYi中使用富文本编辑器&#xff0c;这部分的内容是向&#xff22;站程序员青戈学习的&#xff0c;当然我这里就会把学到的内容做一个总结&#xff0c;当然也会说…

Java基础语法---集合---ArrayList

ArrayList是什么 ArrayList可以看作是一个动态数组&#xff0c;提供了自动扩容的能力&#xff0c;意味着它能够根据需要自动调整其大小以容纳更多的元素&#xff0c;而无需预先指定数组的容量。 使用ArrayList需要加入包 import java.util.ArryList ArrayList与普通数组的不同…

【C++】─篇文章带你熟练掌握 map 与 set 的使用

目录 一、关联式容器二、键值对三、pair3.1 pair的常用接口说明3.1.1 [无参构造函数](https://legacy.cplusplus.com/reference/utility/pair/pair/)3.1.2 [有参构造函数 / 拷贝构造函数](https://legacy.cplusplus.com/reference/utility/pair/pair/)3.1.3 [有参构造函数](htt…

bootstrap5-学习笔记1-容器+布局+按钮+工具

参考&#xff1a; Bootstrap5 教程 | 菜鸟教程 https://www.runoob.com/bootstrap5/bootstrap5-tutorial.html Spacing Bootstrap v5 中文文档 v5.3 | Bootstrap 中文网 https://v5.bootcss.com/docs/utilities/spacing/ 之前用bootstrap2和3比较多&#xff0c;最近用到了5&a…

【FAS】《Liveness Detection on Face Anti-spoofing》

文章目录 原文总结与评价CNN-RNN vs 三维卷积作者的方法 原文 [1]欧阳文汉.反人脸图像欺诈的活体识别方法研究[D].浙江大学,2020.DOI:10.27461/d.cnki.gzjdx.2020.002675. 总结与评价 时序运动信息与传统的空间纹理信息相结合 基于相位平移的运动放大算法不错 视觉大小细胞…

每日一题《leetcode--LCR 029.循环有序列表的插入》

https://leetcode.cn/problems/4ueAj6/ 这道题整体上想插入数据有三种情况&#xff1a; 1、整个列表是空列表&#xff0c;需要返回插入的结点 2、整个列表只有一个结点&#xff0c;需要在头结点后插入新结点&#xff0c;随机把新结点的next指向头结点 3、整个列表的结点 >1 …

关于飞速(FS)800G光模块的技术问答

随着云计算、物联网&#xff08;IoT&#xff09;和大数据等技术的快速发展&#xff0c;对网络带宽和传输速率的需求越来越大。飞速&#xff08;FS&#xff09;800G光模块的引入旨在满足对高速数据传输的需求&#xff0c;该光模块采用先进的调制解调技术和高密度光电子元件&…

STM32F103VE和STM32F407VE的引脚布局

STM32F103VE vs STM32F407VE 引脚对比表 引脚 STM32F103VE STM32F407VE 备注 1 VSS VSS 地 2 VDD VDD 电源 3 VSSA VSSA 模拟地 4 VDDA VDDA 模拟电源 5 OSC_IN OSC_IN 外部时钟输入 6 OSC_OUT OSC_OUT 外部时钟输出 7 NRST NRST 复位 8 PC13 (GPIO) PC13 (GPIO) GPIO 9 PC14 (…

鸿蒙全栈开发-一文读懂鸿蒙同模块不同模块下的UIAbility跳转详解

前言 根据第三方机构Counterpoint数据&#xff0c;截至2023年三季度末&#xff0c;HarmonyOS在中国智能手机操作系统的市场份额已经提升至13%。短短四年的时间&#xff0c;HarmonyOS就成长为仅次于安卓、苹果iOS的全球第三大操作系统。 因此&#xff0c;对于鸿蒙生态建设而言&a…

依赖注入方式和自动加载原理

依赖注入 Spring提供了依赖注入的功能&#xff0c;方便我们管理和使用各种Bean&#xff0c;常见的方式有&#xff1a; 字段注入&#xff08;Autowired 或 Resource&#xff09;构造函数注入set方法注入 在以往代码中&#xff0c;我们经常利用Spring提供的Autowired注解来实现…