全面升级!一套基于最新版Spring Cloud的微服务实战项目!

最近把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在电商业务的基础集成了注册中心、配置中心、监控中心、网关等系统功能。

后台管理系统演示

后台管理系统演示地址:https://www.macrozheng.com/admin/index.html

移动端商城演示

移动端商城演示地址(浏览器切换到手机模式体验更佳):https://www.macrozheng.com/app/

系统架构

mall-swarm采用目前主流的微服务技术栈实现,涵盖了一般项目中几乎所有使用的技术。同时项目业务完整,包括前台商城和后台管理系统,能支持完整订单流程,通过下面这张架构图,大家应该能对mall-swarm项目的架构有所了解了。

升级版本

目前项目中的依赖都已经升级到了最新主流版本,具体的版本可以参考下表。

框架版本说明
Spring Cloud2021.0.3->2023.0.1微服务框架
Spring Cloud Alibaba2021.0.1.0->2023.0.1.0微服务框架
Spring Boot2.7.5->3.2.2Java应用开发框架
Spring Boot Admin2.7.5->3.2.2微服务应用监控
Sa-TokenSpring Security Oauth2->Sa-Token认证和授权框架
Nacos2.1.0->2.3.0微服务注册中心
MyBatis3.5.10->3.5.14ORM框架
MyBatisGenerator1.4.1->1.4.2数据层代码生成
PageHelper5.3.2->6.1.0MyBatis物理分页插件
Knife4j3.0.3->4.5.0(SpringFox->SpringDoc)文档生产工具
Druid1.2.14->1.2.21数据库连接池
Hutool5.8.9->5.8.16Java工具类库

升级用法

在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的注解,注解对照关系可以参考下表;
SpringFoxSpringDoc注解用途
@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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值