首页 > 文章列表 > 如何搭建SpringBoot+MyBatisPlus快速开发脚手架

如何搭建SpringBoot+MyBatisPlus快速开发脚手架

springboot mybatisplus
148 2023-05-10

如何搭建SpringBoot+MyBatisPlus快速开发脚手架

    聊聊mall-tiny项目

    可能有些小伙伴还不了解这个脚手架,我们先来聊聊它!

    项目简介

    mall-tiny是一款基于SpringBoot+MyBatis-Plus的快速开发脚手架,目前在Github上已有1100+Star。它拥有完整的权限管理功能,支持使用MyBatis-Plus代码生成器生成代码,可对接mall项目的Vue前端,开箱即用。

    项目演示

    mall-tiny项目可无缝对接mall-admin-web前端项目,秒变前后端分离脚手架,由于mall-tiny项目仅实现了基础的权限管理功能,所以前端对接后只会展示权限管理相关功能。

    技术选型

    这次升级不仅支持了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-ui/

    使用流程

    升级版本基本不影响之前的使用方式,具体使用流程可以参考最新版README文件:

    升级过程

    接下来我们再来聊聊项目升级Spring Boot 2.7.0版本遇到的问题,这些应该是升级该版本的通用问题,你如果想升级2.7.0版本的话,了解下会很有帮助!

    Swagger升级

    • 在升级Spring Boot 2.6.x版本的时候,其实Swagger就有一定的兼容性问题,需要在配置中添加BeanPostProcessor这个Bean,具体可以参考升级 SpringBoot 2.6.x 版本后,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();
    
        }
    
    }
    • 还有一个典型的循环依赖问题,UmsAdminServiceImpl和UmsAdminCacheServiceImpl相互依赖了;

    /**
    
     * 后台管理员管理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);
    
        }
    
    }