整个系统在认证与授权使用的是 spring-security-oauth2 ,早先想做升级的时候看到官方出了个 Authorization Server 。去 Github 和一些社区看看发现这个项目还比较早,且不是很活跃,于是想等到稳定一些。

现在打算升级到 6.0 版本就需要整个 spring 关联框架全部升级。所以整个过程包含以下问题:JDK 17, Gradle,SpringBoot, SpringCloud,数据库相关,重要的独立三方库,Oauth2,业务代码,框架代码。

TL;DR点击列表跳转

  1. 构建环境
  2. Spring相关
  3. Spring Security
  4. 最后

1. 构建环境

1.1 JDK 17

这方面从静态代码的角度来说其实在升级过程中影响并不大。

当把 JDK 17 引入项目后点开项目的 External Libraries 的瞬间我就觉得,无论升级有多麻烦都值得了。和 JDK 8 相比这舒服的模块分类,简直是自己知识体系归类的官方规范。

值得注意的是 @Deprecated 这个注解

1
2
3
4
5
6
7
8
9
10
11
12
13
@Documented  
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
public @interface Deprecated {
/**
* @since 9
**/
String since() default "";
/**
* @since 9
**/
boolean forRemoval() default false;
}

@Deprecated 标记的类或方法,如果 #forRemoval() 为 true 的时候如果再引用好像在编译阶段会直接提示(忘记是错误提示还怎么的了)。这导致了对于基础数据类型包装类的直接构建函数初始化,可能都会出问题。如 Integer , Float 等。

其次对很多从逻辑上看不是很合适的用法也标注了过时,这里面我印象最深刻的是 Calss#newInstance() 方法。官方给出的替代是先获取构造函数再调用实例化。

clazz.newInstance() -> clazz.getDeclaredConstructor().newInstance()

1.2 Gradle

我觉得 Gradle 是每次升级从感观上来说影响最大的部分,无论是开发工具升级(IDEA)还是 Gradle 本身升级。

对于 Gradle 常见的问题比如更新太快教程太少,我觉得这都没什么。在我看来 Gradle 最大的问题是整个构建流程理解起来非常困难,这就导致了报错非常不明显,甚至不报错。比如突然刷新不了脚本,不下载依赖,不编译了,项目在开发工具中不识别了。甚至是写一个 Gradle 公共脚本同一个项目下一个模块引用后运行很好,另一个模块引用报错。

在这里我就不得不说一下自己关于私有库的经历。由于之前使用阿里云的免费私有仓库,在改为 Gradle (使用 Gradle 7.x, 更高版本 IDEA 不支持)后配置了 maven-publish 插件上传非常顺滑,然后切换到一个框架的一个子项目从 repositories 中拉取主框架的依赖。结果报错私有库认证失败。我当时非常无语,可以上传,但是不能下载。随后在 Github 看了下反馈(似乎有个写法不规范的问题),然后去 Gradle 官方在各种版本的文档中仔细看。看来一圈想了想可能是私有库问题(到阿里云看了发现好像有些操作不灵活了),那么就换成阿里云效的免费私有库。测试一下竟然成功了(哭了)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
maven {  
name = "privateSnapshot"
url 'https://packages.aliyun.com/maven/repository/...../'
content {
includeGroup "com....."
}
mavenContent {
snapshotsOnly()
}
credentials{
username "......."
password "......."
}
}

虽说几年前做 android 的时候对于 Gradle 有各种不满情绪,但是当我现在看到对于 repositories 管理的文档后,我就觉得这一切都值得。在 Maven 中我没发现能控制依赖从哪个仓库里下载的功能,这导致了明明我私有库的依赖经常的从阿里云库里请求,然后又去国外几个大型仓库请求在各种提示不存在后才加载我的私有库(这带点在打包发布的时候尤为明显)。

现在 Gradle 提供了 includeGroup "com.xxx"snapshotsOnly() 做仓库加载的限制对我来说真是太好了。

依赖传递

我觉得依赖传递的可控性是非常好的功能,在做项目设计以及业务隔离时有了一个在编译阶段的强限制。

我不知道针对 “模块” 有没有封装和访问控制的说法,当然这里不是指的 spring-security 那种运行时级别的控制。我觉得在编译阶段做模块功能访问的控制,是限制内部功能暴露,甚至是屏蔽具体业务实现的最好方法。在我记忆中 Maven 是无法控制依赖的传递,并且这种传递是编译级别的。这就导致了在开发阶段代码可以访问整个依赖链条上的所有类,怎样就类似与接口间传递枚举类一样,你无法控制这种间接依赖在未来是否变更。

虽然运行阶段大家都在 classpath 中,但是限制了对依赖深度的访问,能避免很多长时间的迭代问题。

Maven BOM

在决定从 Maven 改为 Gradle 的时候我就在思考,maven 里面的项目 parent 功能应该如何实现,于是我看到了关于 Gradle 对于 Maven bom 的支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
plugins {  
id 'org.springframework.boot' version '3.0.4'
id 'io.spring.dependency-management' version '1.1.0'
id 'java'
}
// ...
subprojects {
ext {
springBootVersion = "3.0.4"
springCloudVersion = "2022.0.1"
// ...
}
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
// ...
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:$springCloudVersion"
}
}

就像在 Maven 中的指定 <parent/> 那样,在 mavenBom 节点引入 spring-cloud-dependencies 也可以实现类似的功能。那么如果我们自己也有这么一个基础的框架项目的话,就需要自己搭建一个空项目制作 Maven BOM 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 自己的 BOM
plugins {
id 'maven-publish'
id 'java-platform'
}
// ...
javaPlatform {
allowDependencies()
}
dependencies {
// 引入外部 BOM
api platform('org.springframework.boot:spring-boot-dependencies:3.0.4')
api platform('org.springframework.cloud:spring-cloud-dependencies:2022.0.1')
api platform('org.springframework.security:spring-security-bom:6.0.2')
// 引入三方库,当然也可以有自己的库
constraints {
api 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.1'
api 'org.projectlombok:lombok:1.18.26'
api 'org.mapstruct:mapstruct:1.4.2.Final'
api "org.mapstruct:mapstruct-processor:1.4.2.Final"
// ...
}

将这个空项目发布到仓库后,使用的时候应用我们自己的 BOM 文件就可以了。非常的方便。

说了这么多这里不得不再次说一下 Gradle 使用的烦心事,最重要的就是版本升级太快,文档和教程跟不上。就比如我一开始配置多级项目,以及后来的自制 BOM, 看官方文档和一些教程后根本不起作用。然后还有一点就是开发工具(比如 IDEA)对 Gradle 版本来说是滞后的,这导致根据编辑器提示做修改反而会出错(无语)。

2. Spring相关

2.1 SpringCloud

spring-boot 与 spring-cloud 这里放在一起说。

Spring-Boot 自动配置

我们知道 spring-boot 的自动配置是加载 META-INF 下面的 spring.factories 文件中配置的类名,实际上是通过 SpringFactoriesLoader 读取这个 key-value 结构的文件。这里需要明确的说这种加载功能并未被 Spring 框架废弃,但是基于 spring-boot 读取自动配置类的功能已无法使用。相应的配置类声明文件改为 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 。里面使用换行区分需要配置类的完整类路径(不是文件路径,是点分的类路径)。

这样造成两个后果:

  1. 所有支持自动配置的三方库必须升级到支持此功能的版本,这点问题不大。
  2. 自定义的自动配置类必须使用新规范重新配置,该文件不需要该代码问题也不大。

配置依赖管理

这个可能是 Gradle 和 springboot 的自动配置共同引起的。之前在写框架层的时候有一个模块间调用隔离的功能,就是两个不同的模块间相互调用可能是 API 直接,也可能是服务之间的 RPC。那么就需要根据部署配置以及运行时的依赖关系,动态的添加 RPC 功能,主要就是 openfeign 。这个功能大部分时候运行挺正常的,但是却在一个配置管理功能中莫名报错,在本来不使用 RPC 的环境中莫名的在 classpath 中加了 openfiegn 依赖。这个依赖关系在 Maven 视图中是完全看不到的。

没想到在换了 Gradle 后之前不报错的项目也开始出现相同的错误,更厉害的是在 Gradle 试图和分析依赖关系中都没有,却在 External Libraries 中看到了。不过也好 Gradle 的依赖管理确实清晰了很多,也许容易定位一下吧(无语)。

Spring-Cloud 依赖变更:

之前一直在提的 Netflix OSS 在新版本中终于被移除了,一些自定义功能(如流量控制)在框架中的类就不存在了。不过好在这些几乎都有了替代品(目前没发现问题),并且业务代码仅仅是技术层面的问题。

2.2 Spring

Spring 在大多数时候是 servlet 应用,这其中难免的会使用到 javx.servlet ,有时候也会有 javax.calidation 。这时候对于 javax - > jakarta 的变更虽然比较繁琐,不过也没什么大问题。

这里面最大的问题是 spring 容器对 bean 的管理方式似乎发生了变化。

Bean 依赖管理

在编译阶段的问题解决后运行项目,很可能遇到下面错误:

Requested bean is currently in creation: Is there an unresolvable circular reference?

这中问题一般是由于直接或间接相互依赖的类,在 bean 初始化过程中顺序存在问题。这里有几种情况

  1. 相互依赖的实例中有依赖更深的关系,如动态代理对象,这种时候最直接的方法就是使用 @Lazy 注解。
  2. 单纯的相互引用:如果没有什么特殊情况也可直接使用 @Lazy 注解。
  3. 同一个配置类中一边注入,一边获取。这就不好说了,可能需要写一个代理类演示从 Context 中获取。如果单纯的引用关系,那么就把注入和获取分散开(写在不同地方)。

如果是条件注入,也就是说向容器注入对象可能是外部提供,也可能是默认实现。比如加了各种 @ConditionXxxx ,在注入一个缺省的情况下同时获取。这种时候就需要写一个逻辑的上的代理类,类似于组合模式。内部通过 ApplicationContext 在使用时动态获取注入的实例。

3. Spring Security

如果是 spring-security 这个基础框架升级那么改动并不是很大,大部分 API 都比较稳定,可能变更的是一些使用方式。所以对于已存在的业务逻辑代码影响并不是很大。

但是如果使用了资源管理或者 Oauth2 的内容那么影响就大了,因为在 spring-security-oauth2 中的一个关于 token 管理的包 oauth2.provider 全部不存在了。现在的 Oauth2 功能有很多直接融入了 spring-security 基础库中,资源和授权的部分则使用了新的 spring-security-oauth2-authorization-server 库。

图上图所示, oauth2-client 的功能直接成为了 HttpSecurity#oauth2Client() 的配置(前提是引入 spring-security-oauth2-client )。也正是这个原因才终于确认了 spring-boot-starter-oauth2-client 这个库并没有被废弃掉。那么在当前这个时间点到底可以使用哪些项目呢?

如上图所示总的来说是三个独立类库,但是需要注意的是由于这些项目都没有独立的 springboot 自动配置,并且都是以 spring-security 为基础,所以根据自己的实际需求可以做选择性引入:

  1. spring-security :通过 starter 引入依赖管理,最基础的功能。所以如果只是自定义一下身份认证,管理一下登录这类功能。那么只使用这一个就够了。
  2. oauth2-authorization-server :资源与授权管理,如果用到这些功能只引用这个就够了。虽然整个代码结构还是沿用 org.springframework.security.oauth2 ,但是功能上改动较大,从旧版本升级要从业务级别重新设计了。
  3. osuth2-client :Oauth2 的客户端功能,需要注意的是这个有自动配置功能。由 @EnableWebSecurity 核心注解动态引入,最直观的作用是引入参数注解 @RegisteredOAuth2AuthorizedClient ,方便我们获取 OAuth2AuthorizedClient 对象。

整体来说如果只是使用了 spring-security 基本功能,那么对于版本的提升只需要看看 WebSecurityConfigurationHttpSecurityConfigration 两个配置类就明白了。但是如果牵扯到自定义认证和授权就比较麻烦,因为牵扯到业务入口的 FilterSecurityInterceptor 过滤器,以及整个 AbstractSecurityInterceptor 继承链已经全部标注为已过时。相对应的在认证逻辑中的 AccessDecisionManager , AfterInvocationManager 以及 RunAsManager 也全部标注为过时。这就意味着删除只是时间问题,需要快速迁移到 authorization-server 来。

4. 最后

总的来说本次升级并没有大的麻烦,用已有的测试用例跑一遍也没有发现问题。一开始觉得 JDK 17 和 Gradle 可能是大问题,真正做起来也没有多大困难。反而是最大的问题是 Spring authorization Server ,因为之前写了大量的 token 认证逻辑,在新的项目中这些基础包竟然都不存在了。不过好在看了官方的稳定后,觉得虽然有一些不同的概念,不过都是类似的东西,这就需要重新做一下设计了。