项目经验分享:开发 JuatAuth Plus 的 springboot starter 依赖包
# 项目经验分享:开发JuatAuth Plus的Springboot starter 依赖包
本项目在JustAuth Plus (opens new window)授权框架(简称jap)的基础上,开发其spring boot starter,包括以下六个模块:
- 开发jap提供的四种授权策略的对应的spring boot starter模块:
jap-simple-spring-boot-starter
、jap-oauth2-spring-boot-starter
、jap-social-spring-boot-starter
、jap-oidc-spring-boot-starter
,并将它们插件化,实现按需引入。也就是说,你的web应用若需要相应模块,才添加对应的maven坐标; jap-spring-boot-starter
。提供一个对四种授权策略高度封装的类:JapTemplate
;- 为上述模块抽取出的基础模块
jap-common
,包括上述模块都会用到的一些自动配置类和和工具类等;
除了以上模块之外,为了支持redis作为缓存数据源,也将spring-boot-starter-redis
以插件化的形式引入。
本次分享主要涉及以下几点:
Spring Boot的自动装配过程,并与@Configurarion
相区别,以及@Conditional
系列注解的使用;
将四种授权策略抽取为单独的starter模块,并为了实现四种授权策略的插件化,需要掌握的maven中<optional>
和<scope>
标签;
充分利用Spring的web支持,采用RequestContextHolder
获取当前线程请求上下文,进一步封装JustAuth Plus框架的授权过程;
打包并发布jar包到maven仓库,涉及maven的lifecycle和plugin。
# 自动装配与@ConditionalOnBean
若不熟悉Spring Boot的自动装配使用和原理,可以阅读这篇文章:Spring Boot面试杀手锏————自动配置原理 (opens new window),这里主要分享自动装配与@Configuration
的差别。
# @ConditionalOnBean为什么失效?
以jap-social-spring-boot-starter
模块为例,其中的SocialAutoConfiguration
作为自动配置(auto-configuration)类会创建SocialStrategy
的bean。在jap-spring-boot-starter
模块中SocialOperations
的bean需要只在SocialStrategy
的bean存在的情况下创建。
SocialAutoConfiguration
:
@Configuration
public class SocialAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public SocialStrategy socialStrategy(ApplicationContext applicationContext,
JapBasicProperties basicProperties,
SocialProperties socialProperties,
AuthStateCache authStateCache,
JapCache japCache){
//......略
}
}
2
3
4
5
6
7
8
9
10
11
12
OperationAutoConfiguration$Social
:
@Configuration
@ConditionalOnBean({SocialStrategy.class})//在SocialStrategy的bean存在的情况下才创建SocialOperations bean
static class Social{
@Bean
@ConditionalOnMissingBean
public SocialOperations socialOperations(SocialStrategy socialStrategy,
SocialProperties socialProperties){
//......略
}
}
2
3
4
5
6
7
8
9
10
在debug后发现,其实存在SocialStrategy
的bean,但SocialOperations
的实例并不会因此创建,看起来@ConditionalOnBean
失效了?通过debug发现,OperationAutoConfiguration$Social
居然在SocialAutoConfiguration
之前被加载^1:
原来不是@ConditionalOnBean
失效了,而是配置类的加载顺序有问题。所以,现在需要解决的问题是,如何控制配置类的加载顺序?
想到了@AutoConfigureAfter
,但debug后结果还是一样,似乎这个注解也是无效的,似乎@COnfiguration
注解的配置类并不能控制加载顺序?
在StackOverflow上找到了解决方案:Spring annotation conditionalOnBean not working (opens new window),回答非常精彩:
The javadoc for
@ConditionalOnBean
describes it as:
Conditional
that only matches when the specified bean classes and/or names are already contained in theBeanFactory
.In this case, the key part is "already contained in the
BeanFactory
". Your own configuration classes are considered before any auto-configuration classes. This means that the auto-configuration of theMetricsEndpoint
bean hasn't happened by the time that your own configuration is checking for its existence and, as a result, yourMetricsFormatEndpoint
bean isn't created.One approach to take would be to create your own auto-configuration class (opens new window) for your
MetricsFormatEndpoint
bean and annotate it with@AutoConfigureAfter(EndpointAutoConfiguration.class)
. That will ensure that its conditions are evaluated after theMetricsEndpoint
bean has been defined.
加粗的两点很关键。在此之前我一直认为有@Configuration
注解的配置类和写入META-INF/spring.factories
中的配置类都一样,以为是Spring Boot提供的实现自动配置的两种殊途同归的方式而已。然而@Configuration
早在Spring时期就有,应被称作Externalized Configuration (opens new window),也即上边引用中加粗的***Your own configuration classes***。而META-INF/spring.factories
是Spring Boot提供的,这才是auto-configuration本身。
访问上面的链接create your own auto-configuration class (opens new window),在7.10.2节里有个note:
Auto-configurations must be loaded that way only. Make sure that they are defined in a specific package space and that they are never the target of component scanning. Furthermore, auto-configuration classes should not enable component scanning to find additional components. Specific
@Import
s should be used instead.自动配置类只能被 那种 方式加载。(后面的略)
第一句话非常关键,“那种”方式指的就是在META-INF/spring.factories
文件中指定配置类,而不是采用@Configuration
注解。
了解了两者的区别后,我将所有自动配置类的@Configuration
注解去掉,让它们只通过auto-configuration的方式被加载,再使用上@AutoConfigureAfter
注解,debug后发现配置类的加载顺序得到了控制,所有策略的自动配置类XxxAutoConfiguration
都优先于OperationAutoConfiguration$Xxx
:
# pom.xml中<optional>
和<scope>
元素
以本项目开发的jap-common
和jap-spring-boot-starter
模块的pom.xml
文件中引入的部分maven依赖为例,主要区别<scope>provided</scope>
和<optional>true</optional>
。
首先假设有一个名叫***demo***的maven项目引入了jap-spring-boot-starter
依赖:
<dependency>
<groupId>com.fujieid.jap.spring.boot</groupId>
<artifactId>jap-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
2
3
4
5
而jap-spring-boot-starter
中有jap-common
依赖,因此通过依赖传递,demo项目中也间接引入了jap-common
模块:
在jap-common
的pom.xml
中,有:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.5.4</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.5.4</version>
<scope>provided</scope>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
这两个依赖配置中都有<scope>provided</scope>
元素,这样做想达到的目的是,在demo项目 中引入了jap-common
模块的maven坐标,那它作为jap-common
的子项目,也一定是一个Spring Boot项目,则一定会使用spring-boot-starter
和spring-boot-starter-web
。因此,这两个依赖没有必要参与依赖传递,应该由demo项目自己提供这两个依赖。这样做的考量大概有两点,一是为了避免jar包冲突,避免一些冗余的重复依赖;二是为了让发布的jar包或者war包”瘦身“,此时的jap-common
打包时是不会包含spring-boot-starter
和spring-boot-starter-web
的。
接下来考察<optional>true</optional>
元素,在jap-spring-boot-starter
中:
<dependency>
<groupId>com.fujieid.jap.spring.boot</groupId>
<artifactId>jap-simple-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fujieid.jap.spring.boot</groupId>
<artifactId>jap-social-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fujieid.jap.spring.boot</groupId>
<artifactId>jap-oauth2-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fujieid.jap.spring.boot</groupId>
<artifactId>jap-oidc-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<optional>true</optional>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
可以发现四种策略的starter模块都有<optional>true</optional>
元素,与<scope>provided</scope>
相似的地方是,有<optional>true</optional>
元素的模块仍然不具有依赖传递,且打包时也不会包含该模块。更重要的是区别,有<scope>provided</scope>
元素的maven依赖,如jap-common
中的spring-boot-starter
,表明虽然jap-common
模块不传递该依赖,但是demo项目 作为jap-common
的使用方,你必须自行提供。然而有<optional>true</optional>
元素的依赖就没有那么强制的要求了,它的意思是“可选的”。可以把jap-simple-spring-boot-starter
理解为插件,如果demo项目 中用不上授权策略simple,那完全可以不在demo项目 中引入该依赖。
扩展阅读:Maven中Optional和Scope元素的使用场景,你弄明白了? (opens new window)、Maven 中true和provided之间的区别 (opens new window)
# 利用RequestContextHolder
在jap框架中,所有的授权方法authenticate(...)
都需要传入HttpServletRequest
和HttpServletResponse
参数,如jap-social进行授权:
JapResponse japResponse = socialStrategy.authenticate(config, request, response);
其实在Spring框架下可以利用RequestContextHolder
类:
Holder class to expose the web request in the form of a thread-bound RequestAttributes object. The request will be inherited by any child threads spawned by the current thread if the inheritable flag is set to true.
该类(
RequestContextHolder
)是一个持有web请求的类,web请求是RequestContextHolder
以绑定在线程上的RequestAttributes的形式暴露。……
RequestContextHolder
中有这样一个私有属性:
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<>("Request attributes");
2
是ThreadLocal类型的,也就是说,不同的线程RequestAttributes
是不同的。同时,一个web后台应用的线程通常都是通过web请求触发而创建,因此,其实可以通过这种方式获取request,对jap框架作进一步封装。
HttpServletRequest request = requestAttributes.getRequest();
HttpServletResponse response = requestAttributes.getResponse();
2
扩展阅读:SpringMVC之RequestContextHolder分析 (opens new window)
# Oauth2授权策略
Oauth2是一个很重要的授权策略,我在阅读OAuth 2.0 的四种方式 (opens new window)这篇文章后画了一个授权UML序列图,方便理解。
以授权码方式为例,需要特别注意的地方是,应用服务器实际上只需要提供一个访问接口,也就是redirect_uri参数对应的地址,访问这个地址必须携带code参数。这种授权方式特特别考验对重定向的理解,请注意gitee的oauth服务并不会访问redirect_uri地址,而是让给户浏览器重定向(注意区别转发)到redirect_uri对应的地址(同时携带code参数),让用户浏览器去访问redirect_uri地址。这也是为什么在测试过程中通常redirect_uri可以是localhost,毕竟gitee oauth服务能不能访问到localhost不重要,只要用户浏览器能够访问localhost即可。
# maven的lifecycle和plugin
主要参考maven官方文档:http://maven.apache.org/ref/3.5.0/maven-core/lifecycles.html
# default lifecycle
maven定义了三类lifecycle (opens new window):
- default Lifecycle
- clean Lifecycle
- site Lifecycle
不论是哪类lifecycle,其中都包含多个阶段(phase),而阶段只是一个抽象的概念,它本身不会有具体的执行。某个phase的具体执行是交由绑定到该phase的插件(plugin)来完成。一个plugin会有多个goal,这些goal会最终完成具体的执行。比如compiler插件,就有三个goal可供选择:
需要重点理解的是default lifecycle的定义:
default
lifecycle is defined without any associated plugin. Plugin bindings for this lifecycle are defined separately for every packaging (opens new window):<!-- default lifecycle的定义。只定义了一些列phase,没有为这些phase绑定任何plugin。有点长,截取了一部分 --> <phases> <phase>validate</phase> <phase>initialize</phase> ...... <phase>deploy</phase> </phases>
1
2
3
4
5
6
7
我的理解是,default lifecycle定义的时候没有为其中的phase默认关联一些plugin,理由是,根据packaging的不同,有些phase绑定了一个或多个plugin和相应goal的,而有些phase却没有绑定任何的plugin,这就意味着这个packaging方式在该阶段(phase)不作任何操作。
举个栗子,下边分别是pom packaging和jar packaging (opens new window)。可以很明显的发现,相比于jar packaging,pom packaging只有install和deploy这两个phase绑定(bind)了plugin和相应goal的。但不论是jar还是pom,它们都采用的是default lifecycle,所以虽然pom packaging在complie这个phase没有绑定plugin,但是它还是会经历complie阶段(phase),只是什么都不做而已。
<!-- pom packaging -->
<phases>
<install>
org.apache.maven.plugins:maven-install-plugin:2.4:install
</install>
<deploy>
org.apache.maven.plugins:maven-deploy-plugin:2.7:deploy
</deploy>
</phases>
2
3
4
5
6
7
8
9
<!-- jar packaging -->
<phases>
<process-resources>
org.apache.maven.plugins:maven-resources-plugin:2.6:resources
</process-resources>
<compile>
org.apache.maven.plugins:maven-compiler-plugin:3.1:compile
</compile>
<process-test-resources>
org.apache.maven.plugins:maven-resources-plugin:2.6:testResources
</process-test-resources>
<test-compile>
org.apache.maven.plugins:maven-compiler-plugin:3.1:testCompile
</test-compile>
<test>
org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test
</test>
<package>
org.apache.maven.plugins:maven-jar-plugin:2.4:jar
</package>
<install>
org.apache.maven.plugins:maven-install-plugin:2.4:install
</install>
<deploy>
org.apache.maven.plugins:maven-deploy-plugin:2.7:deploy
</deploy>
</phases>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
下面进行一个测试,对我的项目执行install
阶段。jar 是 maven 默认的 packaging 类型,packaging 未显式指定时为 jar 类型。下面执行的phase和顺序刚好和上面的jar packaing相吻合。
最后看看clean lifecycle的定义,可以发现不像default phase一样,只有光秃秃的定义了一堆phase,这里在定义了一些phase的同时还为一些phase绑定了默认的plugin,比如这里,对clean phase绑定了maven-clean-plugin以及其中的clean目标。
clean
lifecycle is defined directly with its plugin bindings.clean lifecycle直接和它相绑定的plugin一起定义。
<phases> <phase>pre-clean</phase> <phase>clean</phase> <phase>post-clean</phase> </phases> <default-phases> <clean> org.apache.maven.plugins:maven-clean-plugin:2.5:clean </clean> </default-phases>
1
2
3
4
5
6
7
8
9
10
# Available Plugins
在maven官方文档Available Plugins (opens new window)中第一句话很关键:
Maven is - at its heart - a plugin execution framework; all work is done by plugins. Looking for a specific goal to execute? This page lists the core plugins and others. There are the build and the reporting plugins:
maven,本质上是一个基于插件执行的框架;所有的工作都由plugin来完成。在寻找并执行一个具体的goal(目标)吗?本文列出了一些核心的plugin和别的plugin,它们被分为build plugin和report plugin。
扩展阅读:Maven 的 Lifecycle 和 plugins (opens new window)