Backend Development 19 min read

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.

macrozheng
macrozheng
macrozheng
How to Upgrade the Mall Project to Spring Boot 3 & JDK 17 – Complete Guide

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

SpringDocConfig

class:

<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

Bearer

prefix 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‑ik

plugin 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

SecurityFilterChain

with 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

javax

must be changed to

jakarta

after the Jakarta EE transition.

Spring Boot 3.2 has a Parameter Name Retention issue; add

-parameters

to the Maven compiler plugin arguments:

<code>&lt;build&gt;
    &lt;plugins&gt;
        &lt;!-- Resolve SpringBoot 3.2 Parameter Name Retention issue --&gt;
        &lt;plugin&gt;
            &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
            &lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;
            &lt;configuration&gt;
                &lt;compilerArgs&gt;
                    &lt;arg&gt;-parameters&lt;/arg&gt;
                &lt;/compilerArgs&gt;
            &lt;/configuration&gt;
        &lt;/plugin&gt;
    &lt;/plugins&gt;
&lt;/build&gt;
</code>

Alternatively, use

@Qualifier

to 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:17

base image and adjust the

docker‑maven‑plugin

configuration 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.

JavaBackend DevelopmentElasticsearchSpring SecuritySpring Boot 3SpringDoc
macrozheng
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.