Spring Security Remember Me 实现机制

登录之外,很多系统还需要"关闭浏览器后仍保持登录",这就是 remember-me(记住我)。Spring Security 提供两种实现,机制和安全性差别很大。SecurityFilterChain 整体认证流程见 Spring Security。 常见误解:不是"让 session 活得更久" 容易望文生义地以为 remember-me 就是把普通 session 的过期时间设得很长,下次访问时顺便刷新一下 session id。实际上标准做法是两套凭证分层: 登录后先发一个短期 session(比如 30 分钟)。 勾选了 remember-me,再额外发一个长期凭证(cookie 里的 series:token)。 下次访问时如果短期 session 已失效,服务端用长期凭证验证通过后,重新建立一个新的 session,而不是延长旧 session 的寿命。 这样做的原因:短期 session 追求高频、低延迟访问(常放内存/Redis),长期凭证追求低频、可撤销、可审计(放数据库即可)。如果把两者合并成"一个超长有效期的 session",会导致服务端要长期保留大量会话状态,且缺乏按设备单独撤销的能力(下文详述)。 简单哈希令牌(TokenBasedRememberMeServices,默认方式) Cookie 内容: username:expirationTime:md5(username + ":" + expirationTime + ":" + password + ":" + key) 优点:无状态,不查数据库。 缺陷:服务端无法单独让某一台设备的 cookie 失效——除非改密码或改全局 key,那样会让所有设备同时下线;且签名依赖密码本身,密码不变、cookie 一旦泄露就能一直重放。 持久化令牌(PersistentTokenRepository) 数据库维护一张 persistent_logins 表,每一行代表一次登录(一个设备/浏览器): series username token last_used Cookie 里存的是 series:token(两个随机值),不含密码信息。 每次带着 remember-me cookie 的请求认证成功后,token 值会轮换,但 series 不变。判定逻辑发生在服务端:先用 series 查到数据库里当前记录的 token 值,再和客户端发来的 token 比对——相等则正常轮换;series 能查到但 token 不相等,说明这个值是已经被换掉的旧值又出现了一次,PersistentTokenBasedRememberMeServices 会判定为 cookie 窃取(cookie theft),删除该 series 的记录并强制重新登录。 因为每个设备对应独立的 series 行,服务端可以按行删除来单独踢掉一个设备,不影响其他设备的登录状态。这就是"跨设备失效控制":本质是把认证状态从"无状态签名"换成了"以设备为粒度的数据库记录"。 需要澄清一点:series 并不是浏览器指纹或设备硬件 id,它只是"这一次 remember-me 登录时生成、存在这台设备 cookie 里的随机值"。它不校验设备身份,如果 cookie 原样被复制到另一台机器,series 照样验证通过——真正防重放靠的是上面的轮换 + 比对机制,不是设备识别。另外这个 cookie 和普通 cookie 一样,只要没限制 path,每次请求都会被发送,传输频率跟 session cookie 无异;真正低频的是服务端"校验/轮换"这个动作的触发频率(只在 session 失效时才用得上)。 跨设备失效控制的三个能力 可枚举:按 username 查询 persistent_logins 表,能看到该用户当前所有"记住我"的设备。 可单点撤销:删除某一行 series,对应设备下次请求 token 校验失败,强制走正常登录。 可全量撤销:改密码时清空该用户所有 series 记录,实现"改密码后所有设备强制下线"。 与 JWT refresh token 的对照 OAuth2 / JWT 体系里常见的 refresh token,跟这里的长期凭证是同一类角色:都是"低频使用、用来重建短期凭证"的东西。但安全性取决于实现方式: ...

Created 26266-12-26 · Updated 2026-07-02 · 2 min · 258 words · -

HikariCP: 连接池参数与泄漏排查

HikariCP 是 Spring Boot 2.x 及以上版本的默认数据库连接池,spring-boot-starter-jdbc/spring-boot-starter-data-jpa 在类路径上检测到它时会自动创建对应的 DataSource,见 spring-boot-relational-persistence.md。 核心设计取向 HikariCP 相比 DBCP、C3P0 等老连接池,主打「轻量、少锁竞争」:连接获取路径尽量避免同步阻塞,字节码层面做了不少微优化(如去掉不必要的包装类)。这也是它成为默认选型的主要原因——同等硬件下吞吐更高、延迟更低。 关键参数 参数 作用 建议 maximumPoolSize 连接池最大连接数 不是越大越好,经验公式 连接数 = (核心数 * 2) + 有效磁盘数;通常 10-20 已足够,过大会导致数据库端连接争用、上下文切换开销上升 minimumIdle 最小空闲连接数 建议和 maximumPoolSize 设成一致,避免连接数动态伸缩带来的抖动(官方推荐固定池大小) connectionTimeout 从池中获取连接的等待超时 默认 30s,业务上一般调到 3-5s,超时快速失败优于线程一直阻塞 idleTimeout 空闲连接被回收前的最长空闲时间 默认 10 分钟,仅在 minimumIdle < maximumPoolSize 时生效 maxLifetime 连接最大生命周期 默认 30 分钟,必须比数据库或负载均衡器的连接超时(如 MySQL wait_timeout)短,否则会用到已被服务端关闭的连接 leakDetectionThreshold 连接泄漏检测阈值 默认 0(关闭);生产建议设 30000-60000ms,超过该时间连接还未归还池会打印 WARN 日志和借出时的调用栈 application.yml 配置示例: spring: datasource: hikari: maximum-pool-size: 15 minimum-idle: 15 connection-timeout: 3000 max-lifetime: 1500000 leak-detection-threshold: 60000 maxLifetime 为什么防不住连接泄漏 容易有的误解是:既然配了 maxLifetime,超时的连接应该会被自动回收,不会一直泄漏下去。实际上 maxLifetime 只对归还回池中的空闲连接生效,对被业务代码借出、一直没有 close() 的连接不生效。 ...

Created 2026-07-02 · Updated 2026-07-02 · 1 min · 187 words · -

Servlet Filter

Servlet Filter Filter 是 Servlet 规范里的一种组件,位于容器和目标资源(Servlet/JSP)之间,可以在请求到达目标资源之前、响应离开目标资源之后做统一处理。Filter 本身不产生响应内容,只负责预处理和后处理,典型用途包括: 身份校验、权限校验 请求/响应日志记录 请求头、响应头改写 内容压缩、编码转换 多个 Filter 可以按声明顺序串成一条链(Filter Chain),这也是 责任链模式(Chain of Responsibility) 在 Servlet 规范里的经典实现;Spring Security 的 SecurityFilterChain 就是在这套机制上封装出来的应用。 Filter 接口 public interface Filter { default void init(FilterConfig filterConfig) throws ServletException { } void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException; default void destroy() { } } init(FilterConfig):容器创建 Filter 实例后调用一次,用于读取初始化参数、保存 FilterConfig。 doFilter(request, response, chain):每次请求都会调用,是 Filter 的核心方法。 destroy():容器销毁 Filter 前调用一次,用于释放资源。 FilterConfig 提供访问 Filter 名称、初始化参数、ServletContext 的方法。 ...

Created 10106-06-10 · Updated 1016-07-01 · 2 min · 293 words · -

Spring Security

SecurityFilterChain SecurityFilterChain 是 Spring Security 的核心抽象,代表一条作用于 HTTP 请求的安全过滤器链。每个进入应用的请求都会依次经过链中的若干 Filter,完成认证(Authentication)、授权(Authorization)、CSRF 防护、会话管理等工作。 底层机制:从 Servlet Filter 到 Spring Security Servlet 规范本身有 Filter 接口和 FilterChain 接口。Spring Security 通过以下两层代理接入这套机制: DelegatingFilterProxy:在 Servlet 容器(Tomcat)层面注册的 Filter,作用是把请求桥接到 Spring 容器中的 Bean。它本身不做安全逻辑,只是一个"入口代理"。 FilterChainProxy:Spring 容器中的真正实现,也是一个特殊的 Filter。它持有所有注册的 SecurityFilterChain Bean,收到请求后,按顺序匹配第一条能处理该 URL 的链,然后依次执行链中的每个 Filter。 HTTP 请求 → Tomcat FilterChain → DelegatingFilterProxy(Servlet Filter,桥接到 Spring,Bean 名固定为 springSecurityFilterChain) → FilterChainProxy(Spring Bean,收集所有 SecurityFilterChain) → SecurityFilterChain 1(匹配 /api/**) UsernamePasswordAuthenticationFilter BearerTokenAuthenticationFilter ExceptionTranslationFilter AuthorizationFilter ... → SecurityFilterChain 2(匹配 /**) ... ← 链中最后一个 Filter 放行(chain.doFilter()),控制权交回 DelegatingFilterProxy → Tomcat FilterChain 继续执行其余 Filter(如果有) → DispatcherServlet → HandlerMapping 找到目标 Controller 方法 → Controller SecurityFilterChain Bean 如何被收集并接入请求 在 @Configuration 类里写的 @Bean public SecurityFilterChain filterChain(...) 方法,只是往 IoC 容器里注册了一个数据对象——具体实现类是 DefaultSecurityFilterChain,内部就是一个 Filter 列表加一个 RequestMatcher。真正让这份数据接入请求处理流程,还需要另外两步: ...

Created 23236-12-23 · Updated 1016-07-01 · 4 min · 781 words · -

Spring 事务 @Transactional

spring 事务 @Transactional 使用 @Transactional 的要点有: 在DAO 层使用 JdbcTemplate 实现DB操作, 在 Service 的实现类上加上 @Transactional 注解, 不推荐在 Service 接口上加 @Transactional 注解. 需要进行事务控制的方法, 必须是 public 方法, 同时要打上 @Transactional 注解. 也可以在Class上加上 @Transactional 注解, 这样相当于给每个 public 函数加上了 @Transactional 注解, 当然我们还可以在其中的函数上加该注解, 这时候将以函数上的设置为准. @Transactional 失效场景 只有 public 方法生效:private/protected/包级方法上加 @Transactional 不生效,因为 Spring 默认约定只处理 public 方法(CGLIB 虽然能代理 protected 方法,但 Spring 事务模块不支持)。 同类自调用(self-invocation):@Transactional 依赖 Spring AOP 生成的代理对象,只有通过代理调用方法才会走事务逻辑。同一个类内部用 this.xxx() 直接调用会绕过代理,导致事务失效。 @Service public class OrderService { public void placeOrder() { this.updateStock(); // this 调用不经过代理,事务失效 } @Transactional public void updateStock() { ... } } 解决办法:把需要事务的方法拆到另一个 Bean 里,或注入自身代理(AopContext.currentProxy(),需要 exposeProxy=true)。 ...

Created 2026-02-02 · Updated 1016-07-01 · 2 min · 219 words · -

Make Illegal States Unrepresentable:让非法状态不可表达

“Make illegal states unrepresentable”(让非法状态不可表达)是一句常被引用的设计口号,出自 Yaron Minsky(Jane Street)2011 年的博客文章 Effective ML Revisited,文中专门有一节就叫「Make illegal states unrepresentable」,用一个 OCaml 的 connection_state 类型做 before/after 对比。核心主张是:与其允许构造出错误的状态、再靠运行时检查去堵漏洞,不如设计数据结构本身,让错误状态压根编译不过、或者根本没有对应的值可以构造。 它要解决的问题 一个常见的反模式:用一个共享结构 + 若干标志位/可空字段,去表达"实际上互斥的几种状态"。 // 反例:一个类硬塞下多种状态,靠字段组合和约定表达"当前是哪种情况" public class Order { private String status; // "CREATED" / "PAID" / "SHIPPED" / "CANCELLED" private String paymentId; // 只有 status = PAID 之后才该有值 private String trackingNo; // 只有 status = SHIPPED 之后才该有值 private String cancelReason; // 只有 status = CANCELLED 才该有值 } 这段代码里,“没付款却有 trackingNo““状态是 CANCELLED 但 paymentId 和 cancelReason 同时非空"这些本不该存在的组合,在类型系统眼里都是合法值——编译器完全放行,只能靠人读代码、写注释、加运行时校验去维持"约定”。约定总有被忘记、被破坏的一天:少写一个 if、漏掉一次校验,非法组合就悄悄溜进了生产数据。 ...

Created 1016-07-01 · Updated 1016-07-01 · 2 min · 239 words · -

Spring Boot

Spring Boot 是什么 Spring Boot 是 Pivotal (现为 VMware/Broadcom)在 2014 年推出的框架,建立在 Spring Framework 之上,目标是简化整个 Spring 生态的开发、配置和部署。它不只是简化 Spring MVC,同样简化了 Spring Data(数据库访问)、Spring Security(认证授权)、Spring Messaging(消息队列)等全家桶的配置。 核心理念是 “约定优于配置”(Convention over Configuration):框架提前定好"默认约定",只要遵循这些约定,就不需要写任何配置;只有想偏离默认行为时,才需要显式配置。 例如,classpath 里有 spring-boot-starter-web,Spring Boot 就约定你要启动一个 Web 服务,自动完成:注册 DispatcherServlet、内嵌 Tomcat 监听 8080 端口、注册 Jackson 用于 JSON 序列化。你什么都不写,java -jar app.jar 就能跑起来。需要覆盖默认行为时,只在 src/main/resources/application.yml 中写偏离默认值的部分: server: port: 9090 # 只需声明偏离默认值的配置 也可通过命令行参数临时覆盖:java -jar app.jar --server.port=9090,优先级高于 application.yml。 出现背景 Spring Boot(2014)与微服务架构的兴起同期出现。2014 年 Martin Fowler 发表了微服务架构的奠基性文章,微服务要求每个服务独立部署、快速启动、进程隔离。传统的 WAR 包 + 外部 Tomcat 的部署方式与这一需求存在明显矛盾,Spring Boot 正是为了解决这一问题而生。 ...

Created 11116-05-11 · Updated 30306-06-30 · 9 min · 1853 words · wiloon

Java ASM 与运行时字节码织入

背景 BTrace、Arthas、Spring LTW、部分 AOP 框架都会在不改变业务源码的前提下改写字节码。底层常依赖 ASM(或其它字节码库)配合 JVM 的 java.lang.instrument API。 本文说明 ASM 做什么、运行时织入怎么走,以及与 Attach API / Agent 的关系。BTrace 的 attach 与脚本下发见 BTrace。概念总图见 Java 领域知识关系图。ClassLoader 可见性见 java classloader。 JLS 与 java.lang.instrument 分别指什么 JLS(Java Language Specification):Java 语言规范,定义语法、类型、可见性等;不规定 Agent 怎么 attach。 java.lang.instrument:JDK 里的标准 API 包,定义 Instrumentation、ClassFileTransformer,以及 Agent 入口约定(premain / agentmain 的方法签名、manifest 的 Premain-Class / Agent-Class)。 日常说「走 instrument 机制」= 用这套 API + JVM 内置实现,在类加载或 retransform 时回调你的 transformer。 Agent 框架定义了什么、解决什么问题 Agent 不是一个限制能力的沙箱,而是一套标准钩子 + 能力接口: 约定 / 能力 作用 manifest Agent-Class / Premain-Class 告诉 JVM 加载 agent 后调用哪个类的哪个入口 agentmain / premain + Instrumentation 在 JVM 启动后或 attach 后执行你的初始化逻辑 ClassFileTransformer 在类字节码进入 JVM 前(或 retransform 时)允许你替换字节码 Attach API loadAgent 运行中的 JVM 也能挂上 agent 要解决的问题:在不重启、不改磁盘上已部署 jar 的前提下,对已在内存中的类做字节码级增强(监控、诊断、部分场景下的热修复)。 ...

Created 15156-12-15 · Updated 30306-06-30 · 3 min · 580 words · -

Spring CGLIB 动态代理

CGLIB(Code Generation Library)是一个运行期生成字节码的代码生成库,能在不实现接口的情况下,为一个类动态创建子类代理。Spring 内部大量依赖它做方法拦截(method interception)——无论是 AOP 代理还是 @Configuration 类的单例保证,背后用的都是 Spring 自己维护的一份 CGLIB fork(org.springframework.cglib),而不是外部独立发布的 cglib.jar。本文以 Spring 里的 CGLIB 为主线;原始独立 CGLIB 项目的来历、维护现状作为背景信息放在文末。 为什么需要 CGLIB:JDK 动态代理的限制 JDK 自 1.3 起自带动态代理(java.lang.reflect.Proxy),使用简单,但有一个硬性限制:被代理的类必须实现一个或多个接口,代理对象本质上是这些接口的另一份实现。如果要代理一个没有实现任何接口的普通类,JDK 动态代理就无能为力了——这正是 CGLIB 存在的原因。 CGLIB 的做法是:在运行期通过字节码处理框架 ASM,动态生成目标类的一个子类,重写其中的非 final 方法,在方法体里插入拦截逻辑,再调用原方法。因为是继承实现,所以不要求目标类实现接口,但也带来限制:类和方法不能是 final(无法被继承和重写),构造函数会被调用(生成的子类需要调用父类构造器)。 CGLIB 和 ASM 的分工:CGLIB 只关心「给目标类生成一个什么样的子类代理」这个业务语义层面的问题——该重写哪些方法、方法体里怎么织入拦截逻辑、生成的类怎么加载成 Class 对象并实例化;至于「怎么把这份子类描述翻译成合法的 .class 字节码」,全部委托给 ASM 完成。可以理解成:CGLIB 决定生成一个什么样的子类,ASM 负责把这个子类真正写成字节码——CGLIB 是构建在 ASM 之上、面向「动态生成子类代理」这个具体场景的应用层库,ASM 才是真正操作字节码指令的底层工具。 JDK 动态代理 CGLIB 实现方式 实现目标接口 生成目标类的子类 前提条件 目标类必须实现接口 目标类和方法不能是 final 底层机制 反射(InvocationHandler) ASM 生成字节码 典型使用方 Spring AOP(默认,接口存在时) Spring AOP(无接口时)、Hibernate 延迟加载、EasyMock/jMock Spring 如何使用 CGLIB:两个典型场景 1. Spring AOP 方法拦截 Spring AOP 优先用 JDK 动态代理;只有当目标类没有实现任何接口时,才回退(或显式配置 proxyTargetClass=true)使用 CGLIB 生成子类代理,在方法调用前后插入切面逻辑(比如 @Transactional 开启/提交事务)。 ...

Created 15156-12-15 · Updated 30306-06-30 · 2 min · 408 words · -

2026 W26

enx 加 Java 后端 最近投了 Java 方向的职位,这段时间在回忆和熟悉 Java/Spring,也在整理 blog 里之前攒下的资料。但只看文档、不动手,理解很有限。过去做 Java 项目时训练过的"生物神经网络"痕迹应该还在,细节被修剪掉了,主要的连接还在,得花点时间把细节重新接上。 enx 是我写的一个英文学习工具,也是技术栈的 playground。这周给 enx 加了一个 Spring Boot 后端,把一个简单的 API 迁了过去,依赖走 Homelab 里的 Nexus Maven 私服,构建和部署接到现有的 Tekton + k8s 流水线上:推代码后 Tekton 自动打镜像、更新到集群。这样一个最小闭环先跑起来,后面再慢慢补。 梳理 Java/Spring 文档 接着 W25 的节奏,继续整理 blog 里的 Java 文档,这周重心移到 Spring 和 JVM 周边。大脑像是索引和内存,blog 像是外部存储;脑子里的东西大概是 blog 内容的一个子集加上索引,有交叉,但也不是所有东西都写进了 blog。整理的过程就像是顺着索引把外存里的细节重新梳理一下。 这几个文档是本周动过的,方便以后自己回溯:classloader、Spring IoC 与 Spring Boot、graalvm native image、HotSpot,以及顺手补的 SPI / ServiceLoader 和 YAGNI。 公司:领照、刻章、开户 W24 注册了公司,这周把领营业执照、刻章办了,银行开户排到了下周。本来 6 月 24 日计划去领照,车启动打开导航才发现当天大连有达沃斯论坛限行,只好折回,在家准备面试和注册材料。第二天才正式跑完一圈。 ...

Created 28286-06-28 · Updated 30306-06-30 · 1 min · 150 words · -

Java ClassLoader

概述 不同 JVM 实现细节各异,下文只讨论 HotSpot 在 JDK 9 及以后(含当前 JDK 26)的行为。JDK 8 及更早的 ExtClassLoader、jre/lib/ext、sun.misc.Launcher 等表述见文末「JDK 8 及更早」。 本文从四方面展开:内置 ClassLoader 层次、双亲委托、自定义 ClassLoader、打破双亲委托的常见场景。类加载器「去哪里找类」由 classpath / module path 决定,概念见 Java Classpath 与 Module Path。 内置 ClassLoader 层次 JDK 运行时提供三个内置加载器(ClassLoader 官方文档): 加载器 获取方式 职责 Bootstrap Java 代码中为 null 加载 java.base 等核心模块;C++ 实现,不是 ClassLoader 子类 Platform(原 Extension) ClassLoader.getPlatformClassLoader() 加载 Java SE 平台 API 及其模块(非应用 classpath) System / Application ClassLoader.getSystemClassLoader() 加载应用 classpath、module path 上的类 父子关系: Bootstrap (null) └── PlatformClassLoader └── AppClassLoader (System ClassLoader) 实现类位于 jdk.internal.loader.ClassLoaders(PlatformClassLoader、AppClassLoader),均为内部 BuiltinClassLoader 体系。不要依赖具体类名写业务逻辑。 ...

Created 9096-09-09 · Updated 28286-06-28 · 3 min · 550 words · -

java -cp / -classpath

作用 -cp 与 -classpath 等价,用来指定运行时查找类和 JAR 的 classpath(类路径)。classpath 的概念、与依赖管理工具和 module path 的关系,见 Java Classpath 与 Module Path。 语法 java -cp <类路径> <主类全限定名> [应用参数...] 条目之间的分隔符:Linux/macOS 用冒号 :,Windows 用分号 ; 条目可以是目录或 JAR/ZIP 文件 . 代表当前目录 支持通配符 lib/*(匹配目录下所有 JAR,不递归子目录),但不能写 lib/*.jar 一旦显式指定 -cp,默认的当前目录 . 不再自动加入,需要时要显式写上 示例 # 当前目录 + 一个 jar(Linux/macOS) java -cp .:lib/hsqldb.jar org.hsqldb.Server -database mydb # Windows 用分号分隔 java -cp .;lib\hsqldb.jar org.hsqldb.Server -database mydb # 用通配符引入一个目录下所有 jar java -cp "app.jar:lib/*" com.example.Main 现在大多用 Maven/Gradle 或 IDE 自动拼装 classpath,手写 -cp 多见于命令行手动编译运行、排查问题等场景。 ...

Created 17176-04-17 · Updated 28286-06-28 · 1 min · 87 words · -

Java ClassLoader (merged into classloader)

本文已合并 这篇 2012 年的转载内容(基于 JDK 8 的 ExtClassLoader、sun.misc.Launcher、jre/lib/ext 等)已过时,相关内容已并入并按 JDK 9+ / 当前 HotSpot 重写: 类加载器层次、双亲委托、自定义 ClassLoader、打破双亲委托:见 Java ClassLoader classpath / module path 概念:见 Java Classpath 与 Module Path 本页仅作旧链接(/p4316/、java-classloader-2)重定向占位保留。 维护记录 时间 修改内容 原因 2026-06-28 退役为重定向占位,正文并入 classloader.md;新增 classpath 概念链到 classpath.md;分类 Java→language,删除 reprint 与现行 HotSpot 不符、与 classloader.md 高度重复

Created 26266-09-26 · Updated 28286-06-28 · 1 min · 47 words · lcf

Java Classpath 与 Module Path

Classpath 是什么 Classpath(类路径) 是 JVM 在运行时查找 .class 文件和 JAR 包的搜索路径集合。当代码里出现 new Foo() 或 Foo.class 时,类加载器要把 com.example.Foo 这个名字解析成磁盘上的 com/example/Foo.class 字节码,去哪里找,就由 classpath 决定。 可以把它理解成「类的 PATH 环境变量」:shell 的 PATH 决定 ls、git 这些命令去哪些目录找可执行文件;JVM 的 classpath 决定加载类时去哪些目录和 JAR 里找字节码。 一条 classpath 由多个条目组成,每个条目是一个目录或一个 JAR/ZIP 文件: 目录条目:以该目录为根,按包名展开找 .class(如条目 target/classes + 类 com.foo.Bar → 找 target/classes/com/foo/Bar.class) JAR 条目:把 JAR 当成一个打包的目录,在其中按同样规则找 分隔符与操作系统有关:Linux/macOS 用冒号 :,Windows 用分号 ;。 如何设置 classpath 按优先级从高到低,常见几种方式: 方式 写法 说明 -cp / -classpath java -cp app.jar:lib/* com.foo.Main 命令行显式指定,最常用;详见 java -cp CLASSPATH 环境变量 export CLASSPATH=... 全局生效,易污染,现代项目基本不用 可执行 JAR 的 Class-Path MANIFEST.MF 里的 Class-Path: JAR 内清单声明依赖;见 MANIFEST.MF 默认值 不指定时为当前目录 . 仅在没有任何设置时 -cp 支持通配符 lib/*(匹配该目录下所有 JAR,但不递归子目录),但不能写成 lib/*.jar。指定了 -cp 后默认的当前目录 . 会失效,需要的话要显式写进去。 ...

Created 28286-06-28 · Updated 28286-06-28 · 2 min · 295 words · -

Spring MVC

Spring MVC 是 Spring Framework 中负责处理 HTTP 请求的 Web 框架,实现了 MVC 设计模式。它是一个功能模块,而非独立框架。 本文专注 HTTP 处理与现用实践(注解映射、REST、校验等)。Spring Framework 整体定位、非 Web 场景(如 CLI)、早期 XML 配置、与 JDK 5 注解的关系、Web 层历史演变,见 Spring。 Spring MVC 与 Spring Boot 的关系 这两者经常被混淆,理清层次是关键。 层次关系 Spring Framework(基础框架) └── Spring MVC(Web 层模块,属于 Spring Framework 的一部分) Spring Boot(构建在 Spring Framework 之上的脚手架/工具) └── spring-boot-starter-web(包含 Spring MVC + 内嵌 Tomcat + Jackson) Spring MVC 是 Spring Framework 中负责处理 HTTP 请求的 Web 框架,实现了 MVC 设计模式。它是一个功能模块。 Spring Boot 是一个构建工具/脚手架,它并不替代 Spring MVC,而是让配置 Spring MVC(以及其他模块)变得更简单。 对比:传统 Spring MVC vs Spring Boot 方面 传统 Spring MVC Spring Boot 配置方式 XML(web.xml、applicationContext.xml)或 Java Config 自动配置,几乎零 XML 部署方式 打包为 WAR,部署到外部 Tomcat 打包为可执行 JAR,内嵌 Tomcat 依赖管理 手动指定每个依赖及版本,容易版本冲突 Starter 统一管理,版本经过验证 启动入口 通过 Servlet 容器启动 @SpringBootApplication 主类 + main() 方法 学习曲线 较陡,需理解大量 XML 配置 较平,约定优于配置 底层请求处理链路 无论是传统 Spring MVC + WAR,还是 Spring Boot + JAR,处理 HTTP 请求的调用链从未改变: ...

Created 30306-11-30 · Updated 27276-06-27 · 4 min · 810 words · -

Java SPI 与 ServiceLoader

是什么 SPI(Service Provider Interface,服务提供者接口)是 Java 内置的插件机制:允许第三方在不修改调用方代码的前提下,提供接口的实现。 核心思路一句话: 调用方只依赖接口 → 实现由 ServiceLoader 在运行时发现 调用方编译期只认接口,不写死任何实现类;具体用哪个实现,由 classpath 上的实现库通过约定文件「自报家门」,由 java.util.ServiceLoader 在运行时加载并实例化。这与 IoC 的思想类似——把装配的控制权移到程序之外,在模块化设计里尤其重要。 三个组成部分 组成 说明 接口(Service) 由框架或标准库定义,如 java.sql.Driver 实现(Provider) 由第三方库提供,如 com.mysql.cj.jdbc.Driver 注册文件 META-INF/services/<接口全限定名>,内容是实现类的全限定名 注册文件规则: 文件名是服务接口的完全限定二进制名称 每行一个实现类的全限定名;空行、行首 # 注释会被忽略 文件必须使用 UTF-8 编码 提供者类必须有无参构造方法,以便被反射实例化 工作流程 ① 调用方:ServiceLoader.load(Driver.class) ↓ ② JVM 扫描 classpath 上所有 jar 里的 META-INF/services/java.sql.Driver ↓ ③ 读出文件中的实现类名,反射实例化 ↓ ④ 返回一个可迭代所有实现的迭代器 ServiceLoader 以延迟方式查找并实例化:维护已加载实现的缓存,iterator() 先返回缓存中的元素,再按需查找剩余实现并依次加入缓存,可用 reload() 清缓存。 一个完整例子 接口与两个实现: package com.example.api; public interface Greeting { void sayHi(); } public class GreetingBeijing implements Greeting { public void sayHi() { System.out.println("hi from beijing"); } } public class GreetingZhengzhou implements Greeting { public void sayHi() { System.out.println("hi from zhengzhou"); } } 注册文件 src/main/resources/META-INF/services/com.example.api.Greeting: ...

Created 7076-11-07 · Updated 27276-06-27 · 2 min · 237 words · -

GraalVM Native Image 简介

是什么 GraalVM Native Image 是 GraalVM 提供的 AOT(Ahead-Of-Time) 编译路径:在构建期把 Java 应用及其依赖静态分析,编译成本地可执行文件(Linux/macOS/Windows 上的单一二进制或小型目录),运行时不再依赖传统意义上的 java -jar + 完整 JRE/HotSpot 进程模型。 与日常默认的 HotSpot 路径对比: HotSpot: 源码 → javac → .class → java 启动 JVM → 解释/JIT → 运行 Native Image: 源码 → javac → .class → native-image 构建 → 本地二进制 → 直接运行 GraalVM 平台总览见 GraalVM 简介。作为运行时有两种常见用法,不要混为一谈: 作为 HotSpot 上的高性能 JIT 编译器(仍是 java 命令跑字节码) 作为 Native Image 工具链(产出独立可执行文件,本文主题) 为什么用它 维度 HotSpot + JAR Native Image 冷启动 秒级常见(JVM 启动、类加载、Spring 扫描) 毫秒级常见 内存占用 完整 JVM 堆与元空间 无 HotSpot 堆模型;通常更小 镜像/分发 需 JRE 或容器内 Java 运行时 单一二进制 + 可选少量资源文件 峰值吞吐 JIT 预热后通常更强 无 C2 长时间优化,长时间 CPU 密集型可能不如 HotSpot 构建 mvn package 较快 native-image 构建慢,CI 时间显著增加 典型场景:Serverless/函数计算、CLI 工具、Kubernetes 中短生命周期且副本多的 Pod、对启动延迟敏感的边缘服务。 ...

Created 21216-06-21 · Updated 27276-06-27 · 4 min · 667 words · -

Spring Boot Executable JAR: Fat JAR 与 JarLauncher

背景 Spring Boot 把应用打成单个可执行 JAR,java -jar app.jar 即可启动内嵌 Tomcat(或 Jetty/Undertow)与全部依赖。这与传统「WAR 部署到外部 Tomcat、classpath 由容器管理」不同,也不同于 Maven jar-with-dependencies 那种把依赖 class 解压合并进一个 jar 的做法。 本文说明 Spring Boot 可执行 JAR 的内部布局、JarLauncher 如何启动应用,以及 repackage / bootJar 如何生成该产物。容器镜像、分层 JAR、Buildpack 等部署话题见 Spring Boot Container Packaging;框架层面的「内嵌服务器 + 可执行 JAR」见 Spring Boot。 Thin jar 与 fat jar Fat jar、uber jar、executable jar 在 Spring Boot 语境下指同一件事:一个「全包」可执行 jar。Fat jar 与 uber jar 只是不同叫法(uber 来自德语 über,意为「之上 / 全部」);Spring 官方文档更常写 executable jar。 与 Maven/Gradle 默认打出的普通 jar(常称 thin jar / plain jar)对比如下: ...

Created 24246-06-24 · Updated 27276-06-27 · 5 min · 973 words · -

Spring WebFlux: 响应式 Web 框架

Spring WebFlux 是 Spring 5(2017)引入的响应式 Web 框架,与基于 Servlet 的 Spring MVC 并列。Boot 总览见 Spring Boot;若只需提升并发能力而不改编程模型,可先看 Java Virtual Threads。 解决的问题 传统 Spring MVC 基于 Servlet 模型,每个请求占用一个线程,调用数据库或外部 API 时线程阻塞等待: 请求进来 → 分配线程 → 调用数据库(等待 100ms,线程阻塞)→ 调用外部 API(等待 200ms,线程阻塞)→ 返回 Tomcat 默认线程池约 200 个线程,高并发下线程池耗尽,后续请求排队。 Spring WebFlux 引入响应式编程模型,底层使用 Netty(非 Servlet 容器),少量线程处理大量并发: 请求进来 → 少量线程(CPU 核数)→ 发起数据库查询(注册回调,线程立刻去处理其他请求)→ 数据返回时触发回调继续处理 编程模型 WebFlux 使用 Reactor 库的 Mono<T>(0 或 1 个异步结果)和 Flux<T>(0 到 N 个异步结果): // Spring MVC(同步阻塞) @GetMapping("/users/{id}") public User getUser(@PathVariable Long id) { return userService.findById(id); // blocks on DB } // Spring WebFlux(异步非阻塞) @GetMapping("/users/{id}") public Mono<User> getUser(@PathVariable Long id) { return userService.findById(id); // returns a deferred result } WebFlux 的局限 编程模型与传统命令式完全不同,调试困难,学习成本高 需要支持响应式的驱动(R2DBC 替代 JDBC),不是所有库都有响应式版本 Java 21 虚拟线程的出现大幅削弱了 WebFlux 在普通业务系统里的核心优势(见 Java Virtual Threads) 何时仍选 WebFlux SSE 实时推送 需要背压控制(消费者告知生产者降速)的流式处理 已有 WebFlux 项目:继续用即可,不必为并发改回 MVC Java 网络层全景 主流 Java Web 栈最终归为两大谱系: ...

Created 26266-06-26 · Updated 27276-06-27 · 2 min · 283 words · wiloon

Spring

Spring 通常指 Spring Framework——一个以 IoC 容器为核心的 Java 企业应用基础框架。它不是专门为 Web 而生的;Web 处理只是其众多模块之一。要理解 Spring MVC、Spring Boot 在生态里的位置,需要先弄清 Spring Framework 本身解决什么问题、各模块如何分工,以及它在 J2EE 时代是如何演进来的。 Spring Framework 是什么 Spring Framework 提供: IoC 容器:创建对象、装配依赖、管理生命周期(详见 Spring IoC,依赖注入) AOP:横切关注点(日志、事务、安全等)与业务代码分离(详见 aop) 数据访问与事务:JDBC 抽象、@Transactional、与 Hibernate / JPA / MyBatis 集成 Web 层:Spring MVC(详见 Spring MVC) 集成能力:消息、邮件、调度任务等 典型分层关系: Spring Framework(基础框架) ├── Core(IoC、AOP、事件、资源抽象) ├── Data Access(JDBC、ORM 集成、事务) ├── Spring MVC(Web 层:DispatcherServlet、Controller) ├── Integration(JMS、邮件等) └── 以及 Security、Batch 等扩展项目 Spring Boot(2014,构建在 Spring Framework 之上) └── 自动配置 + Starter,简化上述模块的装配与部署(详见 [Spring Boot](./spring-boot.md)) Spring 与 Spring MVC 的关系 Spring MVC 是 Spring Framework 里的 Web 模块,不是与 Spring 平级的独立框架。 ...

Created 24246-06-24 · Updated 26266-06-26 · 6 min · 1207 words · -