Stories

Detail Return Return

全面升級!一套基於最新版Spring Cloud的微服務實戰項目! - Stories Detail

最近把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

user avatar dewujishu Avatar shoushoudeqie Avatar startshineye Avatar
Favorites 3 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.