最近把mall-swarm項目升級支持了最新版Spring Cloud+Spring Boot 3+JDK17,今天就來介紹下mall-swarm項目做了哪些升級,包括依賴的升級、框架的用法升級以及運行部署的改動,希望對大家有所幫助!
mall-swarm項目簡介
這裏還是簡單介紹下mall-swarm項目吧,mall-swarm項目(11k+star)是一套微服務商城系統,採用了Spring Cloud Alibaba、Spring Boot 3.2、JDK17、Kubernetes等核心技術,同時提供了基於Vue的管理後台方便快速搭建系統。mall-swarm在電商業務的基礎集成了註冊中心、配置中心、監控中心、網關等系統功能。
- Github地址:https://github.com/macrozheng/mall-swarm
- Gitee地址:https://gitee.com/macrozheng/mall-swarm
- 文檔網站:https://cloud.macrozheng.com
後台管理系統演示
後台管理系統演示地址:https://www.macrozheng.com/admin/index.html
移動端商城演示
移動端商城演示地址(瀏覽器切換到手機模式體驗更佳):https://www.macrozheng.com/app/
系統架構
mall-swarm採用目前主流的微服務技術棧實現,涵蓋了一般項目中幾乎所有使用的技術。同時項目業務完整,包括前台商城和後台管理系統,能支持完整訂單流程,通過下面這張架構圖,大家應該能對mall-swarm項目的架構有所瞭解了。
升級版本
目前項目中的依賴都已經升級到了最新主流版本,具體的版本可以參考下表。
| 框架 | 版本 | 説明 |
|---|---|---|
| Spring Cloud | 2021.0.3->2023.0.1 | 微服務框架 |
| Spring Cloud Alibaba | 2021.0.1.0->2023.0.1.0 | 微服務框架 |
| Spring Boot | 2.7.5->3.2.2 | Java應用開發框架 |
| Spring Boot Admin | 2.7.5->3.2.2 | 微服務應用監控 |
| Sa-Token | Spring Security Oauth2->Sa-Token | 認證和授權框架 |
| Nacos | 2.1.0->2.3.0 | 微服務註冊中心 |
| MyBatis | 3.5.10->3.5.14 | ORM框架 |
| MyBatisGenerator | 1.4.1->1.4.2 | 數據層代碼生成 |
| PageHelper | 5.3.2->6.1.0 | MyBatis物理分頁插件 |
| Knife4j | 3.0.3->4.5.0(SpringFox->SpringDoc) | 文檔生產工具 |
| Druid | 1.2.14->1.2.21 | 數據庫連接池 |
| Hutool | 5.8.9->5.8.16 | Java工具類庫 |
升級用法
在mall-swarm項目升級Spring Boot 3的過程中,有些框架的用法有所改變,比如微服務權限解決方案改用了Sa-Token,微服務API文檔聚合方案中的Knife4j實現改用了SpringDoc,商品搜索功能中使用了Spring Data Elasticsearch的新用法,這裏我們將着重講解這些升級的新用法!
微服務權限解決方案升級
由於之前使用的基於Spring Security Oauth2權限解決方案已經不再支持Spring Boot 3,這裏改用了Sa-Token提供的微服務權限解決方案。
- 在mall-gateway網關服務上進行了比較大的改動,比如之前使用
AuthorizationManager來實現動態權限,現在使用了SaReactorFilter來實現動態權限;
/**
* @auther macrozheng
* @description Sa-Token相關配置
* @date 2023/11/28
* @github https://github.com/macrozheng
*/
@Configuration
public class SaTokenConfig {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 註冊Sa-Token全局過濾器
*/
@Bean
public SaReactorFilter getSaReactorFilter(IgnoreUrlsConfig ignoreUrlsConfig) {
return new SaReactorFilter()
// 攔截地址
.addInclude("/**")
// 配置白名單路徑
.setExcludeList(ignoreUrlsConfig.getUrls())
// 鑑權方法:每次訪問進入
.setAuth(obj -> {
// 對於OPTIONS預檢請求直接放行
SaRouter.match(SaHttpMethod.OPTIONS).stop();
// 登錄認證:商城前台會員認證
SaRouter.match("/mall-portal/**", r -> StpMemberUtil.checkLogin()).stop();
// 登錄認證:管理後台用户認證
SaRouter.match("/mall-admin/**", r -> StpUtil.checkLogin());
// 權限認證:管理後台用户權限校驗
// 獲取Redis中緩存的各個接口路徑所需權限規則
Map<Object, Object> pathResourceMap = redisTemplate.opsForHash().entries(AuthConstant.PATH_RESOURCE_MAP);
// 獲取到訪問當前接口所需權限(一個路徑對應多個資源時,擁有任意一個資源都可以訪問該路徑)
List<String> needPermissionList = new ArrayList<>();
// 獲取當前請求路徑
String requestPath = SaHolder.getRequest().getRequestPath();
// 創建路徑匹配器
PathMatcher pathMatcher = new AntPathMatcher();
Set<Map.Entry<Object, Object>> entrySet = pathResourceMap.entrySet();
for (Map.Entry<Object, Object> entry : entrySet) {
String pattern = (String) entry.getKey();
if (pathMatcher.match(pattern, requestPath)) {
needPermissionList.add((String) entry.getValue());
}
}
// 接口需要權限時鑑權
if(CollUtil.isNotEmpty(needPermissionList)){
SaRouter.match(requestPath, r -> StpUtil.checkPermissionOr(Convert.toStrArray(needPermissionList)));
}
})
// setAuth方法異常處理
.setError(this::handleException);
}
}
- 對於需要登錄認證和權限的功能的應用模塊,比如mall-admin和mall-portal模塊,添加了Sa-Token整合Redis的依賴,從而實現了基於Redis的分佈式Session,之後需要登錄用户信息的時候就可以直接從Session中去獲取了;
<!-- Sa-Token 整合 Redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>${sa-token.version}</version>
</dependency>
- 對於認證中心mall-auth模塊,之前使用Spring Security Oauth2時的登錄邏輯比較複雜,現在改成了直接遠程調用擁有登錄邏輯的模塊實現登錄,代碼邏輯更加簡潔了。
/**
* @auther macrozheng
* @description 統一認證授權接口
* @date 2024/1/30
* @github https://github.com/macrozheng
*/
@Controller
@Tag(name = "AuthController", description = "統一認證授權接口")
@RequestMapping("/auth")
public class AuthController {
@Autowired
private UmsAdminService adminService;
@Autowired
private UmsMemberService memberService;
@Operation(summary = "登錄以後返回token")
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public CommonResult login(@RequestParam String clientId,
@RequestParam String username,
@RequestParam String password) {
if(AuthConstant.ADMIN_CLIENT_ID.equals(clientId)){
UmsAdminLoginParam loginParam = new UmsAdminLoginParam();
loginParam.setUsername(username);
loginParam.setPassword(password);
return adminService.login(loginParam);
}else if(AuthConstant.PORTAL_CLIENT_ID.equals(clientId)){
return memberService.login(username,password);
}else{
return CommonResult.failed("clientId不正確");
}
}
}
Knife4j微服務文檔聚合方案升級
Knife4j是一個基於Swagger的API文檔增強解決方案,由於之前使用的Swagger庫為SpringFox,目前已經不支持Spring Boot 3了,這裏遷移到了SpringDoc。
- 遷移到SpringDoc後,Knife4j API文檔的使用和之前基本一致,訪問地址還是原來的:http://localhost:8201/doc.html
- 之前在Controller和實體類上使用的SpringFox的註解,需要改用SpringDoc的註解,註解對照關係可以參考下表;
| SpringFox | SpringDoc | 註解用途 |
|---|---|---|
| @Api | @Tag | 用於接口類,標識這個類是Swagger的資源,可用於給接口類添加説明 |
| @ApiIgnore | @Parameter(hidden = true) or @Operation(hidden = true) or @Hidden |
忽略該類的文檔生成 |
| @ApiImplicitParam | @Parameter | 隱式指定接口方法中的參數,可給請求參數添加説明 |
| @ApiImplicitParams | @Parameters | 隱式指定接口方法中的參數集合,為上面註解的集合 |
| @ApiModel | @Schema | 用於實體類,聲明一個Swagger的模型 |
| @ApiModelProperty | @Schema | 用於實體類的參數,聲明Swagger模型的屬性 |
| @ApiOperation(value = "foo", notes = "bar") | @Operation(summary = "foo", description = "bar") | 用於接口方法,標識這個類是Swagger的一個接口,可用於給接口添加説明 |
| @ApiParam | @Parameter | 用於接口方法參數,給請求參數添加説明 |
| @ApiResponse(code = 404, message = "foo") | ApiResponse(responseCode = "404", description = "foo") | 用於描述一個可能的返回結果 |
- 由於Knife4j的實現改用了SpringDoc,有一點需要
特別注意,添加認證請求頭時,已經無需添加Bearer前綴,SpringDoc會自動幫我們添加的。
Spring Data Elasticsearch新用法
Spring Data ES中基於ElasticsearchRepository的一些簡單查詢的用法是沒變化的,對於複雜查詢,由於ElasticsearchRestTemplate類已經被移除,需要使用ElasticsearchTemplate類來實現。
- 使用ElasticsearchTemplate實現的複雜查詢,對比之前變化也不大,基本就是一些類和方法改了名字而已,大家可以自行參考
EsProductServiceImpl類中源碼即可;
/**
* 搜索商品管理Service實現類
* Created by macro on 2018/6/19.
*/
@Service
public class EsProductServiceImpl implements EsProductService {
private static final Logger LOGGER = LoggerFactory.getLogger(EsProductServiceImpl.class);
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
@Override
public Page<EsProduct> search(String keyword, Long brandId, Long productCategoryId, Integer pageNum, Integer pageSize,Integer sort) {
Pageable pageable = PageRequest.of(pageNum, pageSize);
NativeQueryBuilder nativeQueryBuilder = new NativeQueryBuilder();
//分頁
nativeQueryBuilder.withPageable(pageable);
//過濾
if (brandId != null || productCategoryId != null) {
Query boolQuery = QueryBuilders.bool(builder -> {
if (brandId != null) {
builder.must(QueryBuilders.term(b -> b.field("brandId").value(brandId)));
}
if (productCategoryId != null) {
builder.must(QueryBuilders.term(b -> b.field("productCategoryId").value(productCategoryId)));
}
return builder;
});
nativeQueryBuilder.withFilter(boolQuery);
}
//搜索
if (StrUtil.isEmpty(keyword)) {
nativeQueryBuilder.withQuery(QueryBuilders.matchAll(builder -> builder));
} else {
List<FunctionScore> functionScoreList = new ArrayList<>();
functionScoreList.add(new FunctionScore.Builder()
.filter(QueryBuilders.match(builder -> builder.field("name").query(keyword)))
.weight(10.0)
.build());
functionScoreList.add(new FunctionScore.Builder()
.filter(QueryBuilders.match(builder -> builder.field("subTitle").query(keyword)))
.weight(5.0)
.build());
functionScoreList.add(new FunctionScore.Builder()
.filter(QueryBuilders.match(builder -> builder.field("keywords").query(keyword)))
.weight(2.0)
.build());
FunctionScoreQuery.Builder functionScoreQueryBuilder = QueryBuilders.functionScore()
.functions(functionScoreList)
.scoreMode(FunctionScoreMode.Sum)
.minScore(2.0);
nativeQueryBuilder.withQuery(builder -> builder.functionScore(functionScoreQueryBuilder.build()));
}
//排序
if(sort==1){
//按新品從新到舊
nativeQueryBuilder.withSort(Sort.by(Sort.Order.desc("id")));
}else if(sort==2){
//按銷量從高到低
nativeQueryBuilder.withSort(Sort.by(Sort.Order.desc("sale")));
}else if(sort==3){
//按價格從低到高
nativeQueryBuilder.withSort(Sort.by(Sort.Order.asc("price")));
}else if(sort==4){
//按價格從高到低
nativeQueryBuilder.withSort(Sort.by(Sort.Order.desc("price")));
}
//按相關度
nativeQueryBuilder.withSort(Sort.by(Sort.Order.desc("_score")));
NativeQuery nativeQuery = nativeQueryBuilder.build();
LOGGER.info("DSL:{}", nativeQuery.getQuery().toString());
SearchHits<EsProduct> searchHits = elasticsearchTemplate.search(nativeQuery, EsProduct.class);
if(searchHits.getTotalHits()<=0){
return new PageImpl<>(ListUtil.empty(),pageable,0);
}
List<EsProduct> searchProductList = searchHits.stream().map(SearchHit::getContent).collect(Collectors.toList());
return new PageImpl<>(searchProductList,pageable,searchHits.getTotalHits());
}
}
- 目前ES 7.17.3版本還是兼容的,這裏測試了下ES 8.x版本,也是可以正常使用的,需要注意的是如果使用了8.x版本版本,對應的Kibana、Logstash和中文分詞插件analysis-ik都需要使用8.x版本。
其他
- 由於Java EE已經變更為Jakarta EE,包名以
javax開頭的需要改為jakarta,導包時需要注意;
- Spring Boot 3.2 版本會有Parameter Name Retention(不會根據參數名稱去尋找對應name的Bean實例)問題,添加Maven編譯插件參數解決:
<build>
<plugins>
<!--解決SpringBoot 3.2 Parameter Name Retention 問題-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
運行部署
Windows
由於Spring Boot 3最低要求是JDK17,我們在Windows下運行項目時需要配置好項目的JDK版本,其他操作和之前版本運行一樣。
Linux
在打包應用的Docker鏡像時,我們也需要配置項目使用openjdk:17,這裏在項目根目錄下的pom.xml中修改docker-maven-plugin插件配置即可。
由於鏡像使用了openjdk:17,我們在打包鏡像之前還許提前下載好openjdk的鏡像,使用如下命令即可,其他操作和之前版本部署一樣。
docker pull openjdk:17
總結
今天主要講解了mall-swarm項目升級Spring Boot 3版本的一些注意點,這裏總結下:
- 項目中使用的框架版本升級到了最新主流版本;
- 微服務權限解決方案從Spring Security Oauth2遷移到了Sa-Token;
- 微服務器API文檔聚合方案Knife4j的具體實現從SpringFox遷移到了SpringDoc;
- 商品搜索功能實現採用了Spring Data ES的新用法;
- 項目運行部署時需要使用JDK 17版本。
項目地址
https://github.com/macrozheng/mall-swarm