1
头图
最近把mall项目升级支持了Spring Boot 3+JDK17,今天就来介绍下mall项目做了哪些升级,包括依赖的升级、框架的用法升级以及运行部署的改动,目前Spring Boot 3版本代码在mall项目的dev-v3分支下,希望对大家有所帮助!

mall项目简介

这里还是先简单介绍下mall项目吧,mall项目是一套基于 SpringBoot + Vue + uni-app 实现的电商系统(Github标星60K),采用Docker容器化部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能!

项目演示:

升级版本

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

框架版本说明
SpringBoot2.7.5->3.2.2Java应用开发框架
SpringSecurity5.7.4->6.2.1认证和授权框架
MyBatis3.5.10->3.5.14ORM框架
MyBatisGenerator1.4.1->1.4.2数据层代码生成器
SprngDataRedis2.7.5->3.2.2Redis数据操作框架
SprngDataElasticsearch4.4.5->5.2.2Elasticsearch数据操作框架
SprngDataMongoDB3.4.5->4.2.2MongoDB数据操作框架
Druid1.2.14->1.2.21数据库连接池
Hutool5.8.9->5.8.16Java工具类库
PageHelper5.3.2->6.1.0MyBatis物理分页插件
Swagger-UISpringFox->SpringDocAPI文档生成工具
logstash-logback-encoder7.2->7.4Logstash日志收集插件
docker-maven-plugin0.40.2->0.43.3应用打包成Docker镜像的Maven插件

升级用法

在mall项目升级Spring Boot 3的过程中,有些框架的用法有所改变,比如生成API文档的库改用了SpringDoc,Spring Data Elasticsearch和Spring Security随着版本升级,用法也不同了,这里我们将着重讲解这些升级的新用法!

从SpringFox迁移到SpringDoc

由于之前使用的Swagger库为SpringFox,目前已经不支持Spring Boot 3了,这里迁移到了SpringDoc。
  • 迁移到SpringDoc后,在application.yml需要添加SpringDoc的相关配置;
springdoc:
  swagger-ui:
    # 修改Swagger UI路径
    path: /swagger-ui.html
    # 开启Swagger UI界面
    enabled: true
    # 用于配置tag和operation的展开方式,这里配置为都不展开
    doc-expansion: 'none'
  api-docs:
    # 修改api-docs路径
    path: /v3/api-docs
    # 开启api-docs
    enabled: true
  group-configs:
    - group: 'default'
      packages-to-scan: com.macro.mall.controller
  default-flat-param-object: false
  • Java配置也需要做对应修改,具体参考SpringDocConfig配置类的代码;
/**
 * SpringDoc相关配置
 * Created by macro on 2024/3/5.
 */
@Configuration
public class SpringDocConfig implements WebMvcConfigurer {

    private static final String SECURITY_SCHEME_NAME = "Authorization";

    @Bean
    public OpenAPI mallAdminOpenAPI() {
        return new OpenAPI()
                .info(new Info().title("mall后台系统")
                        .description("mall后台相关接口文档")
                        .version("v1.0.0")
                        .license(new License().name("Apache 2.0")
                                .url("https://github.com/macrozheng/mall-learning")))
                .externalDocs(new ExternalDocumentation()
                        .description("SpringBoot实战电商项目mall(60K+Star)全套文档")
                        .url("http://www.macrozheng.com"))
                .addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME))
                .components(new Components()
                        .addSecuritySchemes(SECURITY_SCHEME_NAME,
                                new SecurityScheme()
                                        .name(SECURITY_SCHEME_NAME)
                                        .type(SecurityScheme.Type.HTTP)
                                        .scheme("bearer")
                                        .bearerFormat("JWT")));
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        //配置访问`/swagger-ui/`路径时可以直接跳转到`/swagger-ui/index.html`
        registry.addViewController("/swagger-ui/").setViewName("redirect:/swagger-ui/index.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")用于描述一个可能的返回结果
  • 在我们使用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版本。

Spring Security新用法

升级Spring Boot 3版本后Spring Security的用法也有所变化,比如某些实现动态权限的类已经被弃用了,Security配置改用了函数式编程的方式。
  • 我们之前用于实现动态权限的DynamicAccessDecisionManager和DynamicSecurityFilter类实现的接口均已被弃用,取而代之的是需要实现AuthorizationManager接口;

  • 这里我们创建一个DynamicAuthorizationManager类来实现动态权限逻辑;
/**
 * 动态鉴权管理器,用于判断是否有资源的访问权限
 * Created by macro on 2023/11/3.
 */
public class DynamicAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {

    @Autowired
    private DynamicSecurityMetadataSource securityDataSource;
    @Autowired
    private IgnoreUrlsConfig ignoreUrlsConfig;

    @Override
    public void verify(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
        AuthorizationManager.super.verify(authentication, object);
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext requestAuthorizationContext) {
        HttpServletRequest request = requestAuthorizationContext.getRequest();
        String path = request.getRequestURI();
        PathMatcher pathMatcher = new AntPathMatcher();
        //白名单路径直接放行
        List<String> ignoreUrls = ignoreUrlsConfig.getUrls();
        for (String ignoreUrl : ignoreUrls) {
            if (pathMatcher.match(ignoreUrl, path)) {
                return new AuthorizationDecision(true);
            }
        }
        //对应跨域的预检请求直接放行
        if(request.getMethod().equals(HttpMethod.OPTIONS.name())){
            return new AuthorizationDecision(true);
        }
        //权限校验逻辑
        List<ConfigAttribute> configAttributeList = securityDataSource.getConfigAttributesWithPath(path);
        List<String> needAuthorities = configAttributeList.stream()
                .map(ConfigAttribute::getAttribute)
                .collect(Collectors.toList());
        Authentication currentAuth = authentication.get();
        //判定是否已经实现登录认证
        if(currentAuth.isAuthenticated()){
            Collection<? extends GrantedAuthority> grantedAuthorities = currentAuth.getAuthorities();
            List<? extends GrantedAuthority> hasAuth = grantedAuthorities.stream()
                    .filter(item -> needAuthorities.contains(item.getAuthority()))
                    .collect(Collectors.toList());
            if(CollUtil.isNotEmpty(hasAuth)){
                return new AuthorizationDecision(true);
            }else{
                return new AuthorizationDecision(false);
            }
        }else{
            return new AuthorizationDecision(false);
        }
    }
}
  • 然后在SecurityConfig中使用函数式编程来配置SecurityFilterChain,使用的方法和类和之前基本一致,只是成了函数式编程的方式而已。
/**
 * SpringSecurity相关配置,仅用于配置SecurityFilterChain
 * Created by macro on 2019/11/5.
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private IgnoreUrlsConfig ignoreUrlsConfig;
    @Autowired
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Autowired(required = false)
    private DynamicAuthorizationManager dynamicAuthorizationManager;

    @Bean
    SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.authorizeHttpRequests(registry -> {
            //不需要保护的资源路径允许访问
            for (String url : ignoreUrlsConfig.getUrls()) {
                registry.requestMatchers(url).permitAll();
            }
            //允许跨域请求的OPTIONS请求
            registry.requestMatchers(HttpMethod.OPTIONS).permitAll();
            //任何请求需要身份认证
        })
        //任何请求需要身份认证
        .authorizeHttpRequests(registry-> registry.anyRequest()
            //有动态权限配置时添加动态权限管理器
            .access(dynamicAuthorizationManager==null? AuthenticatedAuthorizationManager.authenticated():dynamicAuthorizationManager)
        )
        //关闭跨站请求防护
        .csrf(AbstractHttpConfigurer::disable)
        //修改Session生成策略为无状态会话
        .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        //自定义权限拒绝处理类
        .exceptionHandling(configurer -> configurer.accessDeniedHandler(restfulAccessDeniedHandler).authenticationEntryPoint(restAuthenticationEntryPoint))
        //自定义权限拦截器JWT过滤器
        .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        return httpSecurity.build();
    }

}

其他

  • 由于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>
  • 或者可以通过在参数上添加@Qualifier指定name来解决,注意如果使用此种方式,Swagger API文档中的请求参数名称也会无法推断,所以还是使用上面的方法吧。
/**
 * @auther macrozheng
 * @description 消息队列相关配置
 * @date 2018/9/14
 * @github https://github.com/macrozheng
 */
@Configuration
public class RabbitMqConfig {

    /**
     * 订单消息实际消费队列所绑定的交换机
     */
    @Bean
    DirectExchange orderDirect() {
        return ExchangeBuilder
                .directExchange(QueueEnum.QUEUE_ORDER_CANCEL.getExchange())
                .durable(true)
                .build();
    }
    
    /**
     * 将订单队列绑定到交换机
     */
    @Bean
    Binding orderBinding(@Qualifier("orderDirect") DirectExchange orderDirect,
                         @Qualifier("orderQueue") Queue orderQueue){
        return BindingBuilder
                .bind(orderQueue)
                .to(orderDirect)
                .with(QueueEnum.QUEUE_ORDER_CANCEL.getRouteKey());
    }
}    

运行部署

Windows

由于Spring Boot 3最低要求是JDK17,我们在Windows下运行项目时需要配置好项目的JDK版本,其他操作和之前版本运行一样。

Linux

在打包应用的Docker镜像时,我们也需要配置项目使用openjdk:17,这里在项目根目录下的pom.xml中修改docker-maven-plugin插件配置即可。

由于镜像使用了openjdk:17,我们在打包镜像之前还许提前下载好openjdk的镜像,使用如下命令即可,其他操作和之前版本部署一样。

docker pull openjdk:17

总结

今天主要讲解了mall项目升级Spring Boot 3版本的一些注意点,这里总结下:

  • 项目中使用的框架版本升级到了最新主流版本;
  • 从SpringFox迁移到了SpringDoc;
  • 商品搜索功能实现采用了Spring Data ES的新用法;
  • Spring Security使用了新用法;
  • 项目运行部署时需要使用JDK 17版本。

项目源码地址

注意Spring Boot 3版本代码在dev-v3分支里。

https://github.com/macrozheng/mall


macrozheng
1.1k 声望1.3k 粉丝