spring boot 应用(二)


文章目录

  • spring boot 应用(二)
  • 数据库版本管理
  • ROM框架 - mybatis plus
  • 实际使用
  • CURD 接口
  • 代码生成
  • 对象转换 - mapstruct
  • BeanUtil.copyProperties
  • MapStruct
  • 全局 API 响应处理
  • 第三方接口调用 - forest



时过一年,在自己spring boot 的项目使用中有了一些小小的改善,添加了一些 数据库和第三方使用请求的框架,用于提高开发的效率。这里只是泛泛的说明使用,不做详细的解说。

数据库版本管理

比较常见的数据库版本管理是 flyway 和 liquibase,flyway 比较简单方便,推荐使用。

ROM框架 - mybatis plus

mybatis plus 官网地址:https://baomidou.com/

开源地址:https://github.com/baomidou/mybatis-plus

对比 tk-mapper(通过mapper)的开源:https://github.com/abel533/Mapper

mybatis plus的star数量比tk-mapper多 4.1 k(2021年1月25日),社区活跃度也是mybatis plus 比较高。

实际使用中的情况,个人觉得mybatis plus 比 tk-mapper好用许多。

maven 依赖

<properties>
    <mybatis.plus.version>3.4.0</mybatis.plus.version>
</properties>
<!--mybatis plus-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>${mybatis.plus.version}</version>
</dependency>

spring boot 配置

@Configuration
// flyway 可以不要
@DependsOn("flywayConfig")
public class MybatisPlusConfig {

    /**
     * 分页插件
     * @Author liurui
     * @Description 分页插件 
     * @Date 9:53 2021/1/19 
     * @param 
     * @return com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor
     **/
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }
}

yaml 配置

mybatis-plus:
  global-config:
    db-config:
      select-strategy: not_empty
  mapper-locations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    auto-mapping-behavior: full
  dbType: mysql
  • select-strategy:select entity 查找的时候不查找空数据
  • mapper-locations:mapper 路径
  • map-underscore-to-camel-case:为true来开启驼峰功能
  • dbType:数据库类型
实际使用

*.xml 文件与 mybatis一样,主要比较常用的是 CRUD 接口、条件构造器、分页插件。

CURD 接口

文件也有很详细的说明,https://baomidou.com/guide/crud-interface.html#mapper-crud-%E6%8E%A5%E5%8F%A3,直接在接口后加 BaseMapper<>

/**
 * <p>
 * 参数映射表 Mapper 接口
 * </p>
 *
 * @author liurui
 * @since 2021-01-20
 */
public interface ParameterComparisionMapper extends BaseMapper<ParameterComparision> {

}

提供的接口够项目初期简单的 curd 使用,这里主要的还是说说代码生成器这一块,貌似现在 tk-mapper 也继承代码生成器,感兴趣的人可以自行研究。

分页也比较简单,直接使用自带的Page类,传入查找方法就行。还能顺便直接查询总数,是个很方便的实现。

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

@Override
    public List<ParameterComparision> getParameterComparisionByType(int type, Page page) {
        QueryWrapper<ParameterComparision> wrapper = new QueryWrapper<>();
        wrapper.eq((type != -1), "parameter_type", type);
        baseMapper.selectPage(page, wrapper);
        return page.getRecords();
    }
代码生成

maven

<!--添加 代码生成器 依赖-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.1.1</version>
</dependency>
<!--模板引擎-->
<dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-engine-core</artifactId>
    <version>2.1</version>
</dependency>
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.28</version>
</dependency>

实现代码

@Slf4j
public class CodeGenerator {

    public static final String DB_URL = "jdbc:mysql://localhost:3306/crawler_platform?serverTimezone=Asia/Shanghai&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true";
    public static final String USER_NAME = "root";
    public static final String PASSWORD = "root";
    public static final String DRIVER = "com.mysql.cj.jdbc.Driver";
    public static final String AUTHOR = "liurui";
    //生成的文件输出到哪个目录
    public static final String OUTPUT_FILE = System.getProperty("user.dir") + "/crawler-platform-mbg/src/main/java";
    //包名,会按照com/example/demo这种形式生成类
    public static final String PACKAGE = "com.***.crawler.core";

    public void generateByTables(boolean serviceNameStartWithI, String... tableNames) {
        GlobalConfig config = new GlobalConfig();
        DataSourceConfig dataSourceConfig = new DataSourceConfig();
        dataSourceConfig.setDbType(DbType.MYSQL)
                .setUrl(DB_URL)
                .setUsername(USER_NAME)
                .setPassword(PASSWORD)
                .setDriverName(DRIVER);
        StrategyConfig strategyConfig = new StrategyConfig();
        strategyConfig
                .setCapitalMode(true)
            // 添加 lombok 依赖
                .setEntityLombokModel(true)
//                .setDbColumnUnderline(true)
                .setNaming(NamingStrategy.underline_to_camel)
                .setInclude(tableNames);//修改替换成你需要的表名,多个表名传数组
        config.setActiveRecord(false)
                .setAuthor(AUTHOR)
                .setOutputDir(OUTPUT_FILE)
                .setOpen(false)
            // xml 生成resultmap 标签
                .setBaseResultMap(true)
                .setBaseColumnList(true)
                .setSwagger2(true)
                .setFileOverride(true);
        if (!serviceNameStartWithI) {
            config.setServiceName("%sService");
        }
        new AutoGenerator().setGlobalConfig(config)
                .setDataSource(dataSourceConfig)
                .setStrategy(strategyConfig)
                .setPackageInfo(
                        new PackageConfig()
                                .setParent(PACKAGE)
                                .setController("controller")
                                .setEntity("entity")
                ).execute();
    }

    public static void main(String[] args) {
        CodeGenerator gse = new CodeGenerator();
        //要给那些表生成
        gse.generateByTables(true, "upload_record", "sync_record");
    }


}

运行后直接生成 controller、service、entity和mapper等代码实体,简化了许多配置,推荐使用。

对象转换 - mapstruct

POJO的各种 PO、VO、BO、DTO对象,不免会互相之间传输成员参数内容,这里有两种传输参数的方法

BeanUtil.copyProperties

直接使用 spring 框架中的 BeanUtil 工具,根据参数名拷贝参数,但是拷贝的过程中会出现部分参数名不一致导致不能完全拷贝全,并且会存在空值覆盖的问题。前一种问题只能一一对应的使用set方法来设置;后一种问题可以跳过空字段来进行赋值。

/**
* 获取需要忽略的属性
*
* @param source
* @return
*/
public static String[] getNullPropertyNames (Object source) {
    final BeanWrapper src = new BeanWrapperImpl(source);
    PropertyDescriptor[] pds = src.getPropertyDescriptors();

    Set<String> emptyNames = new HashSet<>();
    for(PropertyDescriptor pd : pds) {
        Object srcValue = src.getPropertyValue(pd.getName());
        // 此处判断可根据需求修改
        if (srcValue == null) {
            emptyNames.add(pd.getName());
        } else if (Number.class.isAssignableFrom(pd.getPropertyType()) || Integer.TYPE.isAssignableFrom(pd.getPropertyType())
                   || srcValue.equals(0)) {
            emptyNames.add(pd.getName());
        }
    }
    String[] result = new String[emptyNames.size()];
    return emptyNames.toArray(result);
}

psvm() {
    // 跳过空字段
    BeanUtils.copyProperties(workerEntity, workerdto, BeanUtil.getNullPropertyNames(workerEntity));
}
MapStruct

接口的形式自定义转换规则

maven

<properties>	
	<mapstruct.version>1.3.0.Final</mapstruct.version>
</properties>
<!--mapstruct-->
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>${mapstruct.version}</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>${mapstruct.version}</version>
</dependency>

<!-- 指定jdk -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <source>${java.version}</source>
        <target>${java.version}</target>
        <encoding>UTF-8</encoding>
        <annotationProcessorPaths>
            <path>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </path>
            <path>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>${mapstruct.version}</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

这里需要注意的是,如果项目使用了 lombok 的注解,否则 mapstruct 会判断没有 get和set 方法,需要在编译的时候指定使用 lombok

使用

@Mapper(componentModel = "spring",
        unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface PageVOConverter {
    PageVOConverter INSTANCE = Mappers.getMapper(PageVOConverter.class);

    /**
     * <p>视图分页转换</p>
     * <p>@Author liurui</p>
     * <p>@Description 视图分页转换</p>
     * <p>@Date 10:59 2021/1/23</p>
     *
     * @param vo
     * @return com.baomidou.mybatisplus.extension.plugins.pagination.Page
     **/
    Page pageParam2Page(PageParamVO vo);

    /**
     * <p>分页结果转视图类</p>
     * <p>@Author liurui</p>
     * <p>@Description 分页结果转视图类</p>
     * <p>@Date 11:03 2021/1/23</p>
     *
     * @param
     * @return com.newgrand.crawler.web.vo.PageParamVO
     **/
    PageParamVO page2PageParam(Page page);
}

psvm() {
    PageParamVO vo = PageVOConverter.INSTANCE.page2PageParam(page1);     
}

使用起来非常方便,如果存在不一样的字段名,可以用 @Mapping() 来进行映射。具体使用可以自行查找或底下留言。

全局 API 响应处理

过去前后端响应内容都是项目封装好的 response 类,响应 code 都一样,调用方法就一直重复写代码,例如:

/**
 * 工人管理controller
 *
 * @author liurui
 */

@Controller
@Api(tags = "工人管理")
@Slf4j
public class WorkerController {

    @GetMapping(value = "/**")
    public Result A(){
		...
        return ResultGenerator.genSuccessResult(result);
    }

    @GetMapping("/***")
    public Result B(){
		....
        return ResultGenerator.genSuccessResult(result);
    }
      @GetMapping(value = "/**")
    public Result C(){
		...
        return ResultGenerator.genSuccessResult(result);
    }

    @GetMapping("/***")
    public Result D(){
		....
        return ResultGenerator.genSuccessResult(result);
    }
}

出现多个 ResultGenerator.genSuccessResult 的响应内容,身为一个懒惰的程序员,这样现象不可容忍。

自定义统一响应注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Controller
@ResponseBody
public @interface ResponseResultBody {

}

实现注解内容

@Slf4j
@RestControllerAdvice
public class ResponseResultBodyAdvice implements ResponseBodyAdvice<Object> {
    private static final Class<? extends Annotation> ANNOTATION_TYPE = ResponseResultBody.class;

    /**
     * 判断类或者方法是否使用了 @ResponseResultBody
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ANNOTATION_TYPE) || returnType.hasMethodAnnotation(ANNOTATION_TYPE);
    }

    /**
     * 当类或者方法使用了 @ResponseResultBody 就会调用这个方法
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 防止重复包裹的问题出现
        if (selectedContentType.getType().equals("image")) {
            return body;
        }
        if (body instanceof Result) {
            return body;
        }
        return ResultGenerator.genSuccessResult(body);
    }

}

往后的在 controller 的类名上加 @ResponseResultBody 就可以不需要重复写代码,想放回什么格式的内容都行。

第三方接口调用 - forest

http client 的一个开源工具,替代传统的 Post/Get 方法调用第三方接口。