How to Upgrade the Mall Project to Spring Boot 3 & JDK 17 – Complete Guide
This article walks through upgrading the open‑source Mall e‑commerce system to Spring Boot 3 and JDK 17, covering dependency version updates, migration from SpringFox to SpringDoc, new Spring Data Elasticsearch usage, revised Spring Security configuration, Docker deployment tips, and essential code changes.
Mall Project Overview
The Mall project is a SpringBoot + Vue + uni‑app e‑commerce system with Dockerized deployment, boasting over 60K GitHub stars. It includes a front‑end store and a back‑end admin system that support the full order workflow, covering products, orders, carts, permissions, coupons, members, payments, and more.
Project address: https://github.com/macrozheng/mall
Video tutorials: https://www.macrozheng.com/video/
Upgraded Versions
All dependencies have been upgraded to the latest mainstream versions:
SpringBoot: 2.7.5 → 3.2.2 (Java application framework)
SpringSecurity: 5.7.4 → 6.2.1 (Authentication and authorization framework)
MyBatis: 3.5.10 → 3.5.14 (ORM framework)
MyBatisGenerator: 1.4.1 → 1.4.2 (Data‑layer code generator)
SpringDataRedis: 2.7.5 → 3.2.2 (Redis data‑operation framework)
SpringDataElasticsearch: 4.4.5 → 5.2.2 (Elasticsearch data‑operation framework)
SpringDataMongoDB: 3.4.5 → 4.2.2 (MongoDB data‑operation framework)
Druid: 1.2.14 → 1.2.21 (Database connection pool)
Hutool: 5.8.9 → 5.8.16 (Java utility library)
PageHelper: 5.3.2 → 6.1.0 (MyBatis pagination plugin)
Swagger‑UI: SpringFox → SpringDoc (API documentation tool)
logstash‑logback‑encoder: 7.2 → 7.4 (Logstash log collector plugin)
docker‑maven‑plugin: 0.40.2 → 0.43.3 (Maven plugin for building Docker images)
New Usage After Upgrade
During the upgrade to Spring Boot 3, several frameworks changed their usage. Notably, the API documentation library switched from SpringFox to SpringDoc, and both Spring Data Elasticsearch and Spring Security have new APIs.
Migrating from SpringFox to SpringDoc
SpringFox no longer supports Spring Boot 3, so migration to SpringDoc is required.
After migration, add SpringDoc configuration to
application.yml:
<code>springdoc:
swagger-ui:
path: /swagger-ui.html
enabled: true
doc-expansion: 'none'
api-docs:
path: /v3/api-docs
enabled: true
group-configs:
- group: 'default'
packages-to-scan: com.macro.mall.controller
default-flat-param-object: false
</code>Java configuration must also be updated. Example
SpringDocConfigclass:
<code>@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) {
// Configure /swagger-ui/ to redirect to /swagger-ui/index.html
registry.addViewController("/swagger-ui/").setViewName("redirect:/swagger-ui/index.html");
}
}
</code>Annotation mapping from SpringFox to SpringDoc:
@Api → @Tag
@ApiIgnore → @Parameter(hidden = true) / @Operation(hidden = true) / @Hidden
@ApiImplicitParam → @Parameter
@ApiImplicitParams → @Parameters
@ApiModel / @ApiModelProperty → @Schema
@ApiOperation → @Operation
@ApiParam → @Parameter
@ApiResponse → ApiResponse
When generating documentation with SpringDoc, the
Bearerprefix is added automatically, so you no longer need to include it in request headers.
Spring Data Elasticsearch New Usage
Simple queries via ElasticsearchRepository remain unchanged. For complex queries, ElasticsearchRestTemplate has been removed; use ElasticsearchTemplate instead (see EsProductServiceImpl ).
<code>/**
* 搜索商品管理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();
// pagination
nativeQueryBuilder.withPageable(pageable);
// filter
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);
}
// search
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()));
}
// sort
if (sort == 1) {
// newest to oldest
nativeQueryBuilder.withSort(Sort.by(Sort.Order.desc("id")));
} else if (sort == 2) {
// sales high to low
nativeQueryBuilder.withSort(Sort.by(Sort.Order.desc("sale")));
} else if (sort == 3) {
// price low to high
nativeQueryBuilder.withSort(Sort.by(Sort.Order.asc("price")));
} else if (sort == 4) {
// price high to low
nativeQueryBuilder.withSort(Sort.by(Sort.Order.desc("price")));
}
// relevance
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());
}
}
</code>ES 7.17.3 remains compatible; ES 8.x also works, but Kibana, Logstash, and the
analysis‑ikplugin must be the same major version.
Spring Security New Usage
After upgrading to Spring Boot 3, some dynamic‑permission classes are deprecated. Implement AuthorizationManager instead.
<code>/**
* 动态鉴权管理器,用于判断是否有资源的访问权限
* 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();
// whitelist paths
List<String> ignoreUrls = ignoreUrlsConfig.getUrls();
for (String ignoreUrl : ignoreUrls) {
if (pathMatcher.match(ignoreUrl, path)) {
return new AuthorizationDecision(true);
}
}
// pre‑flight OPTIONS request
if (request.getMethod().equals(HttpMethod.OPTIONS.name())) {
return new AuthorizationDecision(true);
}
// permission check
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);
}
}
}
</code>Configure
SecurityFilterChainwith functional style:
<code>@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 -> {
// whitelist URLs
for (String url : ignoreUrlsConfig.getUrls()) {
registry.requestMatchers(url).permitAll();
}
// allow OPTIONS for CORS pre‑flight
registry.requestMatchers(HttpMethod.OPTIONS).permitAll();
})
.authorizeHttpRequests(registry -> registry.anyRequest()
.access(dynamicAuthorizationManager == null ? AuthenticatedAuthorizationManager.authenticated() : dynamicAuthorizationManager))
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(configurer -> configurer
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint))
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}
}
</code>Other Changes
Package names that start with
javaxmust be changed to
jakartaafter the Jakarta EE transition.
Spring Boot 3.2 has a Parameter Name Retention issue; add
-parametersto the Maven compiler plugin arguments:
<code><build>
<plugins>
<!-- Resolve SpringBoot 3.2 Parameter Name Retention issue -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</code>Alternatively, use
@Qualifierto specify bean names, but this may prevent Swagger from inferring request‑parameter names.
Running and Deploying
Windows
Spring Boot 3 requires JDK 17. Ensure the project is compiled and run with JDK 17 on Windows; other operations remain the same as previous versions.
Linux / Docker
When building Docker images, use the
openjdk:17base image and adjust the
docker‑maven‑pluginconfiguration in
pom.xml. Pull the image beforehand:
<code>docker pull openjdk:17</code>Summary
All framework dependencies have been upgraded to the latest mainstream versions.
Swagger documentation migrated from SpringFox to SpringDoc.
Product search now uses the new Spring Data Elasticsearch approach.
Spring Security configuration updated to the new functional style.
The project must be built and run with JDK 17.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.