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

macrozheng 發表於 2022-06-10
關注我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-admin-web前端專案,秒變前後端分離腳手架,由於mall-tiny專案僅實現了基礎的許可權管理功能,所以前端對接後只會展示許可權管理相關功能。

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

前端專案地址:https://github.com/macrozheng...

技術選型

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

技術版本說明
SpringBoot2.7.0容器+MVC框架
SpringSecurity5.7.1認證和授權框架
MyBatis3.5.9ORM框架
MyBatis-Plus3.5.1MyBatis增強工具
MyBatis-Plus Generator3.5.1資料層程式碼生成器
Swagger-UI3.0.0文件生產工具
Redis5.0分散式快取
Docker18.09.0應用容器引擎
Druid1.2.9資料庫連線池
Hutool5.8.0Java工具類庫
JWT0.9.1JWT登入支援
Lombok1.18.24簡化物件封裝工具

資料庫表結構

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

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

介面文件

由於升級了Swagger版本,原來的介面文件訪問路徑已經改變,最新訪問路徑:http://localhost:8080/swagger...

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

使用流程

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

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

升級過程

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

Swagger升級

/**
 * 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最新用法

/**
 * 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各種屬性來配置的,具體參考如下程式碼:

/**
 * 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類中的程式碼。

/**
 * 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版本已經開始不推薦使用迴圈依賴了,如果你的專案中使用的迴圈依賴比較多的話,可以使用如下配置開啟;
spring:
  main:
    allow-circular-references: true
  • 不過既然官方都不推薦使用了,我們最好還是避免迴圈依賴的好,這裡分享下我解決迴圈依賴問題的一點思路。如果一個類裡有多個依賴項,這個類非必要的Bean就不要配置了,可以使用單獨的類來配置Bean。比如SecurityConfig這個配置類中,我只宣告瞭必要的SecurityFilterChain配置;
/**
 * 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配置類中,這樣就避免了之前的迴圈依賴;
/**
 * 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相互依賴了;
/**
 * 後臺管理員管理Service實現類
 * Created by macro on 2018/4/26.
 */
@Service
public class UmsAdminServiceImpl extends ServiceImpl<UmsAdminMapper,UmsAdmin> 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的工具類來實現;
/**
 * 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來解決迴圈依賴。
/**
 * 後臺管理員管理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版本時,如果不修改之前的跨域配置,通過前端訪問會出現跨域問題,後端報錯如下。

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來設定,具體配置修改如下。

/**
 * 全域性跨域配置
 * 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...