還在從零開始搭建項目?這款升級版快速開發腳手架值得一試!

語言: CN / TW / HK

關注我Github的小夥伴應該瞭解,之前我開源了一款快速開發腳手架mall-tiny,該腳手架繼承了mall項目的技術棧,擁有完整的權限管理功能。最近抽空把該項目支持了Spring Boot 2.7.0,今天再和大家聊聊這個腳手架,同時聊聊升級項目到Spring Boot 2.7.0的一些注意點,希望對大家有所幫助!

SpringBoot實戰電商項目mall(50k+star)地址:https://github.com/macrozheng/mall

聊聊mall-tiny項目

可能有些小夥伴還不瞭解這個腳手架,我們先來聊聊它!

項目簡介

mall-tiny是一款基於SpringBoot+MyBatis-Plus的快速開發腳手架,目前在Github上已有1100+Star。它擁有完整的權限管理功能,支持使用MyBatis-Plus代碼生成器生成代碼,可對接mall項目的Vue前端,開箱即用。

項目地址:https://github.com/macrozheng/mall-tiny

項目演示

mall-tiny項目可無縫對接mall-admin-web前端項目,秒變前後端分離腳手架,由於mall-tiny項目僅實現了基礎的權限管理功能,所以前端對接後只會展示權限管理相關功能。

前端項目地址:https://github.com/macrozheng/mall-admin-web

技術選型

這次升級不僅支持了Spring Boot 2.7.0,其他依賴版本也升級到了最新版本。

| 技術 | 版本 | 説明 | | ---------------------- | ------- | ---------------- | | SpringBoot | 2.7.0 | 容器+MVC框架 | | SpringSecurity | 5.7.1 | 認證和授權框架 | | MyBatis | 3.5.9 | ORM框架 | | MyBatis-Plus | 3.5.1 | MyBatis增強工具 | | MyBatis-Plus Generator | 3.5.1 | 數據層代碼生成器 | | Swagger-UI | 3.0.0 | 文檔生產工具 | | Redis | 5.0 | 分佈式緩存 | | Docker | 18.09.0 | 應用容器引擎 | | Druid | 1.2.9 | 數據庫連接池 | | Hutool | 5.8.0 | Java工具類庫 | | JWT | 0.9.1 | JWT登錄支持 | | Lombok | 1.18.24 | 簡化對象封裝工具 |

數據庫表結構

化繁為簡,僅保留了權限管理功能相關的9張表,業務簡單更加方便定製開發,覺得mall項目學習太複雜的小夥伴可以先學習下mall-tiny。

接口文檔

由於升級了Swagger版本,原來的接口文檔訪問路徑已經改變,最新訪問路徑:http://localhost:8080/swagger-ui/

使用流程

升級版本基本不影響之前的使用方式,具體使用流程可以參考最新版README文件:https://github.com/macrozheng/mall-tiny

升級過程

接下來我們再來聊聊項目升級Spring Boot 2.7.0版本遇到的問題,這些應該是升級該版本的通用問題,你如果想升級2.7.0版本的話,瞭解下會很有幫助!

Swagger升級

```java /* * Swagger API文檔相關配置 * Created by macro on 2018/4/26. / @Configuration @EnableSwagger2 public class SwaggerConfig extends BaseSwaggerConfig {

@Bean
public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {
    return new BeanPostProcessor() {

        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
                customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
            }
            return bean;
        }

        private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) {
            List<T> copy = mappings.stream()
                    .filter(mapping -> mapping.getPatternParser() == null)
                    .collect(Collectors.toList());
            mappings.clear();
            mappings.addAll(copy);
        }

        @SuppressWarnings("unchecked")
        private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) {
            try {
                Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");
                field.setAccessible(true);
                return (List<RequestMappingInfoHandlerMapping>) field.get(bean);
            } catch (IllegalArgumentException | IllegalAccessException e) {
                throw new IllegalStateException(e);
            }
        }
    };
}

} ```

  • 之前我們通過@Api註解的description屬性來配置接口描述的方法已經被棄用了;

  • 我們可以使用@Tag註解來配置接口説明,並使用@Api註解中的tags屬性來指定。

Spring Security升級

升級Spring Boot 2.7.0版本後,原來通過繼承WebSecurityConfigurerAdapter來配置的方法已經被棄用了,僅需配置SecurityFilterChainBean即可,具體參考Spring Security最新用法

```java /* * SpringSecurity 5.4.x以上新用法配置 * 為避免循環依賴,僅用於配置HttpSecurity * Created by macro on 2019/11/5. / @Configuration public class SecurityConfig {

@Autowired
private IgnoreUrlsConfig ignoreUrlsConfig;
@Autowired
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
@Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private DynamicSecurityService dynamicSecurityService;
@Autowired
private DynamicSecurityFilter dynamicSecurityFilter;

@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
    ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
            .authorizeRequests();
    //不需要保護的資源路徑允許訪問
    for (String url : ignoreUrlsConfig.getUrls()) {
        registry.antMatchers(url).permitAll();
    }
    //允許跨域請求的OPTIONS請求
    registry.antMatchers(HttpMethod.OPTIONS)
            .permitAll();
    // 任何請求需要身份認證
    registry.and()
            .authorizeRequests()
            .anyRequest()
            .authenticated()
            // 關閉跨站請求防護及不使用session
            .and()
            .csrf()
            .disable()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            // 自定義權限拒絕處理類
            .and()
            .exceptionHandling()
            .accessDeniedHandler(restfulAccessDeniedHandler)
            .authenticationEntryPoint(restAuthenticationEntryPoint)
            // 自定義權限攔截器JWT過濾器
            .and()
            .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    //有動態權限配置時添加動態權限校驗過濾器
    if(dynamicSecurityService!=null){
        registry.and().addFilterBefore(dynamicSecurityFilter, FilterSecurityInterceptor.class);
    }
    return httpSecurity.build();
}

} ```

MyBatis-Plus升級

MyBatis-Plus從之前的版本升級到了3.5.1版本,用法沒有大的改變,感覺最大的區別就是代碼生成器的用法改了。 在之前的用法中我們是通過new對象然後set各種屬性來配置的,具體參考如下代碼:

java /** * MyBatisPlus代碼生成器 * Created by macro on 2020/8/20. */ public class MyBatisPlusGenerator { /** * 初始化全局配置 */ private static GlobalConfig initGlobalConfig(String projectPath) { GlobalConfig globalConfig = new GlobalConfig(); globalConfig.setOutputDir(projectPath + "/src/main/java"); globalConfig.setAuthor("macro"); globalConfig.setOpen(false); globalConfig.setSwagger2(true); globalConfig.setBaseResultMap(true); globalConfig.setFileOverride(true); globalConfig.setDateType(DateType.ONLY_DATE); globalConfig.setEntityName("%s"); globalConfig.setMapperName("%sMapper"); globalConfig.setXmlName("%sMapper"); globalConfig.setServiceName("%sService"); globalConfig.setServiceImplName("%sServiceImpl"); globalConfig.setControllerName("%sController"); return globalConfig; } }

而新版的MyBatis-Plus代碼生成器已經改成使用建造者模式來配置了,具體可以參考MyBatisPlusGenerator類中的代碼。

java /** * MyBatisPlus代碼生成器 * Created by macro on 2020/8/20. */ public class MyBatisPlusGenerator { /** * 初始化全局配置 */ private static GlobalConfig initGlobalConfig(String projectPath) { return new GlobalConfig.Builder() .outputDir(projectPath + "/src/main/java") .author("macro") .disableOpenDir() .enableSwagger() .fileOverride() .dateType(DateType.ONLY_DATE) .build(); } }

解決循環依賴問題

  • 其實Spring Boot從2.6.x版本已經開始不推薦使用循環依賴了,如果你的項目中使用的循環依賴比較多的話,可以使用如下配置開啟;

yaml spring: main: allow-circular-references: true

  • 不過既然官方都不推薦使用了,我們最好還是避免循環依賴的好,這裏分享下我解決循環依賴問題的一點思路。如果一個類裏有多個依賴項,這個類非必要的Bean就不要配置了,可以使用單獨的類來配置Bean。比如SecurityConfig這個配置類中,我只聲明瞭必要的SecurityFilterChain配置;

```java /* * SpringSecurity 5.4.x以上新用法配置 * 為避免循環依賴,僅用於配置HttpSecurity * Created by macro on 2019/11/5. / @Configuration public class SecurityConfig {

@Autowired
private IgnoreUrlsConfig ignoreUrlsConfig;
@Autowired
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
@Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private DynamicSecurityService dynamicSecurityService;
@Autowired
private DynamicSecurityFilter dynamicSecurityFilter;

@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
    //省略若干代碼...
    return httpSecurity.build();
}

} ```

  • 其他配置都被我移動到了CommonSecurityConfig配置類中,這樣就避免了之前的循環依賴;

```java /* * SpringSecurity通用配置 * 包括通用Bean、Security通用Bean及動態權限通用Bean * Created by macro on 2022/5/20. / @Configuration public class CommonSecurityConfig {

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

@Bean
public IgnoreUrlsConfig ignoreUrlsConfig() {
    return new IgnoreUrlsConfig();
}

@Bean
public JwtTokenUtil jwtTokenUtil() {
    return new JwtTokenUtil();
}

@Bean
public RestfulAccessDeniedHandler restfulAccessDeniedHandler() {
    return new RestfulAccessDeniedHandler();
}

@Bean
public RestAuthenticationEntryPoint restAuthenticationEntryPoint() {
    return new RestAuthenticationEntryPoint();
}

@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){
    return new JwtAuthenticationTokenFilter();
}

@Bean
public DynamicAccessDecisionManager dynamicAccessDecisionManager() {
    return new DynamicAccessDecisionManager();
}

@Bean
public DynamicSecurityMetadataSource dynamicSecurityMetadataSource() {
    return new DynamicSecurityMetadataSource();
}

@Bean
public DynamicSecurityFilter dynamicSecurityFilter(){
    return new DynamicSecurityFilter();
}

} ```

  • 還有一個典型的循環依賴問題,UmsAdminServiceImplUmsAdminCacheServiceImpl相互依賴了;

```java /* * 後台管理員管理Service實現類 * Created by macro on 2018/4/26. / @Service public class UmsAdminServiceImpl extends ServiceImpl implements UmsAdminService { @Autowired private UmsAdminCacheService adminCacheService; }

/* * 後台用户緩存管理Service實現類 * Created by macro on 2020/3/13. / @Service public class UmsAdminCacheServiceImpl implements UmsAdminCacheService { @Autowired private UmsAdminService adminService; } ```

  • 我們可以創建一個用於獲取Spring容器中的Bean的工具類來實現;

```java /* * Spring工具類 * Created by macro on 2020/3/3. / @Component public class SpringUtil implements ApplicationContextAware {

private static ApplicationContext applicationContext;

// 獲取applicationContext
public static ApplicationContext getApplicationContext() {
    return applicationContext;
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    if (SpringUtil.applicationContext == null) {
        SpringUtil.applicationContext = applicationContext;
    }
}

// 通過name獲取Bean
public static Object getBean(String name) {
    return getApplicationContext().getBean(name);
}

// 通過class獲取Bean
public static <T> T getBean(Class<T> clazz) {
    return getApplicationContext().getBean(clazz);
}

// 通過name,以及Clazz返回指定的Bean
public static <T> T getBean(String name, Class<T> clazz) {
    return getApplicationContext().getBean(name, clazz);
}

} ```

  • 然後在UmsAdminServiceImpl中使用該工具類獲取Bean來解決循環依賴。

java /** * 後台管理員管理Service實現類 * Created by macro on 2018/4/26. */ @Service public class UmsAdminServiceImpl extends ServiceImpl<UmsAdminMapper,UmsAdmin> implements UmsAdminService { @Override public UmsAdminCacheService getCacheService() { return SpringUtil.getBean(UmsAdminCacheService.class); } }

解決跨域問題

在使用Spring Boot 2.7.0版本時,如果不修改之前的跨域配置,通過前端訪問會出現跨域問題,後端報錯如下。

bash java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header. To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.

具體的意思就是allowedOrigins已經不再支持通配符*的配置了,改為需要使用allowedOriginPatterns來設置,具體配置修改如下。

```java /* * 全局跨域配置 * Created by macro on 2019/7/27. / @Configuration public class GlobalCorsConfig {

/**
 * 允許跨域調用的過濾器
 */
@Bean
public CorsFilter corsFilter() {
    CorsConfiguration config = new CorsConfiguration();
    //允許所有域名進行跨域調用
    config.addAllowedOriginPattern("*");
    //該用法在SpringBoot 2.7.0中已不再支持
    //config.addAllowedOrigin("*");
    //允許跨越發送cookie
    config.setAllowCredentials(true);
    //放行全部原始頭信息
    config.addAllowedHeader("*");
    //允許所有請求方法跨域調用
    config.addAllowedMethod("*");
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return new CorsFilter(source);
}

} ```

總結

今天分享了下我的開源項目腳手架mall-tiny,以及它升級SpringBoot 2.7.0的過程。我們在寫代碼的時候,如果有些用法已經廢棄,應該儘量去尋找新的用法來使用,這樣才能保證我們的代碼足夠優雅!

項目地址

開源不易,覺得項目有幫助的小夥伴點個Star支持下吧!

https://github.com/macrozheng/mall-tiny

「其他文章」