掘金 后端 ( ) • 2024-05-06 11:44

来源:mp.weixin.qq.com/s/ehfZ9aXc1…
作者:梦想de星空

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

mall项目简介

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

  • 项目地址:github.com/macrozheng/…
  • 视频教程:www.macrozheng.com/video/

项目演示:

升级版本

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

升级用法

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

从SpringFox迁移到SpringDoc

由于之前使用的Swagger库为SpringFox,目前已经不支持Spring Boot 3了,这里迁移到了SpringDoc。

  • 迁移到SpringDoc后,在application.yml需要添加SpringDoc的相关配置;

  • Java配置也需要做对应修改,具体参考SpringDocConfig配置类的代码;
  • 之前在Controller和实体类上使用的SpringFox的注解,需要改用SpringDoc的注解,注解对照关系可以参考下表;

  • 在我们使用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 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 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 searchHits = elasticsearchTemplate.search(nativeQuery, EsProduct.class); if(searchHits.getTotalHits()<=0){ return new PageImpl<>(ListUtil.empty(),pageable,0); } List 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 { @Autowired private DynamicSecurityMetadataSource securityDataSource; @Autowired private IgnoreUrlsConfig ignoreUrlsConfig; @Override public void verify(Supplier authentication, RequestAuthorizationContext object) { AuthorizationManager.super.verify(authentication, object); } @Override public AuthorizationDecision check(Supplier authentication, RequestAuthorizationContext requestAuthorizationContext) { HttpServletRequest request = requestAuthorizationContext.getRequest(); String path = request.getRequestURI(); PathMatcher pathMatcher = new AntPathMatcher(); //白名单路径直接放行 List 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 configAttributeList = securityDataSource.getConfigAttributesWithPath(path); List 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@EnableWebSecuritypublic 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编译插件参数可以解决:
  • org.apache.maven.plugins maven-compiler-plugin -parameters
  • 或者可以通过在参数上添加@Qualifier指定name来解决,注意如果使用此种方式,Swagger API文档中的请求参数名称也会无法推断,所以还是使用上面的方法吧。
  • /** * @auther macrozheng * @description 消息队列相关配置 * @date 2018/9/14 * @github github.com/macrozheng /@Configurationpublic 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分支里。