芝法酱躺平攻略(1)——SpringBoot 多模块 maven工程搭建

SpringBoot maven工程搭建

  • 一、设计目标
    • 项目结构:
      • 1. 根目录
      • 2. common模块
      • 3. web-common模块
        • 3.1 web模块
        • 3.2 mysql模块
        • 3.3 redis模块
        • 3.4 web-all模块
      • 4. 业务模块
  • 二、maven配置
    • 2.1 IDEA 工程创建
    • 2.2 项目结构
    • 2.3 根pom配置
      • 2.3.1 SpringBoot、cloud、alibaba版本选择
      • 2.3.2 properties
      • 2.3.3 dependencyManagement
      • 2.3.5 profiles
    • 2.4 common的pom配置
      • 2.4.1 common的目标
      • 2.4.2 common pom配置
    • 2.5 web-common模块的pom配置
      • 2.5.1 web-common根模块
      • 2.5.2 framework-web-common模块
      • 2.5.3 framework-mysql
      • 2.5.4 framework-redis
      • 2.5.5 framework-web-all
    • 2.6 业务模块的pom配置
  • 三、common模块
    • 3.1 common目标
    • 3.2 config包
      • 3.2.1 fastjson
      • 3.2.2 yml配置的读取
    • 3.3 entity包
    • 3.3.1 RestResponse
    • 3.3.2 ServiceException
    • 3.3.3 通用返回的一些annotations
    • 3.4 util包
  • 四、web-common模块
    • 4.1 项目目录
    • 4.2 swagger相关配置
    • 4.3 web序列化相关配置
    • 4.4 全局错误捕捉
    • 4.5 全局响应
    • 4.6 DtoUtil
  • 五、业务模块
    • 5.1 本章目标
    • 5.2 代码结构
    • 5.3 配置
      • 5.3.1 配置类
      • 5.3.2 nacos中的配置
    • 5.4 用到的entity
    • 5.5 用户service
    • 5.6 controller
      • 5.6.1 SpringBoot服务启动回调
      • 5.6.2 NacosPropertyApi
      • 5.6.3 用户接口
    • 5.7 测试展示
      • 5.7.1 是用swagger做测试
      • 5.7.2 nacos-property
      • 5.7.3 业务接口测试

一、设计目标

在现实的开发中,往往是多模块开发。把一些共通的代码或配置提取出来,方便业务模块引用。以达到代码复用和快速开发。
在本篇中,我们引入nacos,swagger,在业务服务器中通过接口访问可以读取nacos中配置的值

项目结构:

1. 根目录

负责管理maven引用的版本,设置一些全局变量

2. common模块

负责放一些与servlet服务器无关的maven引用,以及一些与servlet-web无关的共通代码
由于gateway与servlet服务器不兼容,所以要把共通代码分成2份

3. web-common模块

和web功能相关的maven引用、配置、拦截器等。该模块是个聚合模块,其下有4个子模块

3.1 web模块

引入swagger,全局controller返回值封装,全局异常捕捉等

3.2 mysql模块

引入mysql,mybatis-plus,以及mybatis-plus相关的配置

3.3 redis模块

引入redis

3.4 web-all模块

把web,mysql,redis引入,方便业务模块一键引入。
同时也方便一些业务,可能不需要mysql或者redis,排除掉相关的模块

4. 业务模块

具体的业务模块

二、maven配置

2.1 IDEA 工程创建

1)点击file->new->project
把groupId 填为 indi.zhifa.recipe
name和artifactId填为bailan
2)删除src
3)在bailan文件夹创建.gitignore

target/
/*/target

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr

# External tool builders
.externalToolBuilders/

2.2 项目结构

芝法酱躺平攻略(1)——SpringBoot 多模块 maven工程搭建_第1张图片

2.3 根pom配置

<groupId>indi.zhifa.recipegroupId>
<artifactId>bailanartifactId>
<version>1.0-SNAPSHOTversion>

group第一个单词使用com(公司)或indi(个人)
第二个单词使用 公司名(或笔名)
第三个单词使用大项目名
artifactId 使用小项目名

2.3.1 SpringBoot、cloud、alibaba版本选择

在老家做开发,通常使用伪单体模式。引用spring-cloud和阿里巴巴的nacos作为基础组建,方便可能的服务间调用。有的公司使用配置中心,有的公司不使用。
官网上有SpringBoot,SpringCloud和alibaba版本的关系,我们通常选择第二新的版本。
由于我个人比较喜欢长连接的nacos(v>2.0),但Spring2.4相比2.3改动很大,为保证拓展性,决定才用2.4以后版本。
所以我更倾向2021.1的阿里巴巴版本
那么,spring cloud使用2020.0.5,SpringBoot使用2.4.13
故parent如下配置

    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.4.13version>
        <relativePath/>
    parent>

2.3.2 properties

properties一般用来定义各依赖的版本,目前我是这样写的,大家其实可以直接抄

    <properties>
        <spring-boot.version>2.4.13spring-boot.version>
        <spring-cloud.version>2020.0.5spring-cloud.version>
        
        <cloud-alibaba.version>2021.1cloud-alibaba.version>
        
        <nacos-client.version>2.0.20.graalnacos-client.version>
        
        <mybatis-plus.version>3.5.1mybatis-plus.version>
        
        <commons.lang3.version>3.9commons.lang3.version>
        
        <commons-pool2.version>2.11.1commons-pool2.version>
        
        <logback.version>1.2.3logback.version>
        
        <dozen.version>5.5.1dozen.version>
        
        <knife4j.version>3.0.3knife4j.version>
        
        <fastjson.version>2.0.23fastjson.version>
        
        <hutool.version>5.8.5hutool.version>
        
        <maven.compiler.source>11maven.compiler.source>
        <maven.compiler.target>11maven.compiler.target>
    properties>

2.3.3 dependencyManagement

dependencyManagement通常做版本的管理,在根pom配置后,子模块可以不写version,在根pom统一管理。
dependencyManagement中写的内容包括三部分
#1 spring-cloud和阿里巴巴的dependencies的import
#2 子模块的信息,在这里写了,子模块引用就不需要写version了
#3 一般的引用
dependencyManagement 可以单独写一个xml文件,方便子类聚合模块引用
芝法酱躺平攻略(1)——SpringBoot 多模块 maven工程搭建_第2张图片
在根目录的pom,只需要这样写:

        <dependencyManagement>
        <dependencies>
            
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-dependenciesartifactId>
                <version>${spring-cloud.version}version>
                <type>pomtype>
                <scope>importscope>
            dependency>
            
            <dependency>
                <groupId>com.alibaba.cloudgroupId>
                <artifactId>spring-cloud-alibaba-dependenciesartifactId>
                <version>${cloud-alibaba.version}version>
                <type>pomtype>
                <scope>importscope>
            dependency>

            <dependency>
                <groupId>indi.zhifa.recipegroupId>
                <artifactId>bailan-dependencyartifactId>
                <version>${project.version}version>
                <type>pomtype>
                <scope>importscope>
            dependency>

            <dependency>
                <groupId>com.alibaba.nacosgroupId>
                <artifactId>nacos-clientartifactId>
                <version>${nacos-client.version}version>
            dependency>

            
            
            <dependency>
                <groupId>com.baomidougroupId>
                <artifactId>mybatis-plus-extensionartifactId>
                <version>${mybatis-plus.version}version>
            dependency>
            
            <dependency>
                <groupId>com.baomidougroupId>
                <artifactId>mybatis-plus-boot-starterartifactId>
                <version>${mybatis-plus.version}version>
            dependency>
            <dependency>
                <groupId>com.baomidougroupId>
                <artifactId>mybatis-plus-generatorartifactId>
                <version>${mybatis-plus.version}version>
            dependency>
            <dependency>
                <groupId>p6spygroupId>
                <artifactId>p6spyartifactId>
                <version>${p6spy.version}version>
            dependency>

            
            <dependency>
                <groupId>org.freemarkergroupId>
                <artifactId>freemarkerartifactId>
                <version>${freemarker.version}version>
            dependency>

            
            
            <dependency>
                <groupId>com.github.xiaoymingroupId>
                <artifactId>knife4j-spring-boot-starterartifactId>
                <version>${knife4j.version}version>
            dependency>
            
            <dependency>
                <groupId>io.springfoxgroupId>
                <artifactId>springfox-swagger-uiartifactId>
                <version>${springfox.version}version>
            dependency>
            
            <dependency>
                <groupId>net.sf.dozergroupId>
                <artifactId>dozerartifactId>
                <version>${dozen.version}version>
            dependency>
            <dependency>
                <groupId>net.sf.dozergroupId>
                <artifactId>dozer-springartifactId>
                <version>${dozen.version}version>
            dependency>
            <dependency>
                <groupId>io.craftsmangroupId>
                <artifactId>dozer-jdk8-supportartifactId>
                <version>1.0.6version>
            dependency>
            
            <dependency>
                <groupId>org.apache.commonsgroupId>
                <artifactId>commons-lang3artifactId>
                <version>${commons.lang3.version}version>
            dependency>
            
            <dependency>
                <groupId>cn.hutoolgroupId>
                <artifactId>hutool-allartifactId>
                <version>${hutool.version}version>
            dependency>
             
            <dependency>
                <groupId>com.alibaba.fastjson2groupId>
                <artifactId>fastjson2artifactId>
                <version>${fastjson.version}version>
            dependency>
            <dependency>
                <groupId>com.alibaba.fastjson2groupId>
                <artifactId>fastjson2-extension-spring5artifactId>
                <version>${fastjson.version}version>
            dependency>
            
            <dependency>
                <groupId>com.baomidougroupId>
                <artifactId>mybatis-plus-annotationartifactId>
                <version>${mybatis-plus.version}version>
            dependency>
            
            <dependency>
                <groupId>jakarta.xml.bindgroupId>
                <artifactId>jakarta.xml.bind-apiartifactId>
                <version>${xml-bind.version}version>
            dependency>
            <dependency>
                <groupId>com.sun.xml.bindgroupId>
                <artifactId>jaxb-implartifactId>
                <version>${xml-bind.version}version>
                <scope>runtimescope>
            dependency>
            
            <dependency>
                <groupId>com.auth0groupId>
                <artifactId>java-jwtartifactId>
                <version>${jwt.version}version>
            dependency>

        dependencies>
    dependencyManagement>

denpendency.xml
放入各模块的版本,方便在子孙模块中使用

   

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <groupId>indi.zhifa.recipegroupId>
    <artifactId>bailan-dependencyartifactId>
    <version>1.0-SNAPSHOTversion>
    <packaging>pompackaging>

    <properties>
        <spring-boot.version>2.4.13spring-boot.version>
        <spring-cloud.version>2020.0.5spring-cloud.version>
        
        <cloud-alibaba.version>2021.1cloud-alibaba.version>
        
        <maven.compiler.source>11maven.compiler.source>
        <maven.compiler.target>11maven.compiler.target>
    properties>

    <dependencyManagement>
        <dependencies>
            
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-dependenciesartifactId>
                <version>${spring-cloud.version}version>
                <type>pomtype>
                <scope>importscope>
            dependency>
            
            <dependency>
                <groupId>com.alibaba.cloudgroupId>
                <artifactId>spring-cloud-alibaba-dependenciesartifactId>
                <version>${cloud-alibaba.version}version>
                <type>pomtype>
                <scope>importscope>
            dependency>

            <dependency>
                <groupId>indi.zhifa.recipegroupId>
                <artifactId>framework-commonartifactId>
                <version>${project.version}version>
            dependency>

            <dependency>
                <groupId>indi.zhifa.recipegroupId>
                <artifactId>framework-web-commonartifactId>
                <version>${project.version}version>
            dependency>

            <dependency>
                <groupId>indi.zhifa.recipegroupId>
                <artifactId>framework-mysqlartifactId>
                <version>${project.version}version>
            dependency>

            <dependency>
                <groupId>indi.zhifa.recipegroupId>
                <artifactId>framework-redisartifactId>
                <version>${project.version}version>
            dependency>

            <dependency>
                <groupId>indi.zhifa.recipegroupId>
                <artifactId>framework-web-allartifactId>
                <version>${project.version}version>
            dependency>

            ...

        dependencies>
    dependencyManagement>

project>

2.3.5 profiles

profiles 一般用于多环境配置

 <profiles>
        
        <profile>
            <id>localid>
            <properties>
                <package.environment>localpackage.environment>
            properties>
        profile>
        
        <profile>
            <id>devid>
            <properties>
                <package.environment>devpackage.environment>
            properties>
            
            <activation>
                <activeByDefault>trueactiveByDefault>
            activation>
        profile>
        
        <profile>
            <id>prodid>
            <properties>
                <package.environment>prodpackage.environment>
            properties>
        profile>
    profiles>

2.4 common的pom配置

2.4.1 common的目标

本期中,common主要包括以下几个方面
#1 基础类库
#2 spring-cloud相关的依赖
#3 yml配置库
#4 通用的返回值封装
#5 通用的异常
#6 通用util

2.4.2 common pom配置

<parent>
        <artifactId>bailanartifactId>
        <groupId>indi.zhifa.recipegroupId>
        <version>1.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>

    <artifactId>framework-commonartifactId>
    <packaging>jarpackaging>

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-aopartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-validationartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starterartifactId>
        dependency>
        
        
        
        
        
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-loadbalancerartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-openfeignartifactId>
        dependency>

        

        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-lang3artifactId>
        dependency>

        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-pool2artifactId>
        dependency>

        <dependency>
            <groupId>com.alibaba.fastjson2groupId>
            <artifactId>fastjson2artifactId>
        dependency>

        <dependency>
            <groupId>com.alibaba.fastjson2groupId>
            <artifactId>fastjson2-extension-spring5artifactId>
        dependency>

        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <scope>providedscope>
        dependency>
        
        <dependency>
            <groupId>net.sf.dozergroupId>
            <artifactId>dozerartifactId>
        dependency>
        <dependency>
            <groupId>net.sf.dozergroupId>
            <artifactId>dozer-springartifactId>
        dependency>
        <dependency>
            <groupId>io.craftsmangroupId>
            <artifactId>dozer-jdk8-supportartifactId>
        dependency>
        
        <dependency>
            <groupId>cn.hutoolgroupId>
            <artifactId>hutool-allartifactId>
        dependency>
        
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-annotationartifactId>
            <version>${mybatis-plus.version}version>
        dependency>
    dependencies>

2.5 web-common模块的pom配置

2.5.1 web-common根模块

web-common模块是个聚合模块,不引用任何库,只管理子模块

    <parent>
        <artifactId>bailanartifactId>
        <groupId>indi.zhifa.recipegroupId>
        <version>1.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>

    <artifactId>common-webartifactId>
    <packaging>pompackaging>
    <modules>
        <module>framework-web-commonmodule>
        <module>framework-mysqlmodule>
        <module>framework-redismodule>
        <module>framework-web-allmodule>
    modules>

2.5.2 framework-web-common模块

    <artifactId>framework-web-commonartifactId>
    <packaging>jarpackaging>

    <dependencies>
        
        <dependency>
            <groupId>indi.zhifa.recipegroupId>
            <artifactId>framework-commonartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
            <exclusions>
                
                <exclusion>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-starter-jsonartifactId>
                exclusion>
            exclusions>
        dependency>
        
        <dependency>
            <groupId>com.github.xiaoymingroupId>
            <artifactId>knife4j-spring-boot-starterartifactId>
        dependency>
        
        <dependency>
            <groupId>io.springfoxgroupId>
            <artifactId>springfox-swagger-uiartifactId>
        dependency>
        
        










        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
            <exclusions>
                <exclusion>
                    <groupId>com.alibaba.nacosgroupId>
                    <artifactId>nacos-clientartifactId>
                exclusion>
            exclusions>
        dependency>
        
        <dependency>
            <groupId>com.alibaba.nacosgroupId>
            <artifactId>nacos-clientartifactId>
            <version>${nacos-client.version}version>
        dependency>
        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <scope>providedscope>
        dependency>
    dependencies>

2.5.3 framework-mysql

    <artifactId>framework-mysqlartifactId>

    <dependencies>
        <dependency>
            <groupId>indi.zhifa.recipegroupId>
            <artifactId>framework-commonartifactId>
        dependency>
        
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
        dependency>
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-extensionartifactId>
        dependency>
        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <scope>providedscope>
        dependency>
    dependencies>

2.5.4 framework-redis

    <artifactId>framework-redisartifactId>
    <packaging>jarpackaging>

    <dependencies>
        <dependency>
            <groupId>indi.zhifa.recipegroupId>
            <artifactId>framework-commonartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <scope>providedscope>
        dependency>
    dependencies>

2.5.5 framework-web-all

    <artifactId>framework-web-allartifactId>
    <packaging>jarpackaging>

    <dependencies>
        <dependency>
            <groupId>indi.zhifa.recipegroupId>
            <artifactId>framework-web-commonartifactId>
        dependency>
        <dependency>
            <groupId>indi.zhifa.recipegroupId>
            <artifactId>framework-redisartifactId>
        dependency>
        <dependency>
            <groupId>indi.zhifa.recipegroupId>
            <artifactId>framework-mysqlartifactId>
        dependency>
    dependencies>

2.6 业务模块的pom配置

    <artifactId>busyartifactId>
    <packaging>jarpackaging>

    <dependencies>
        
        <dependency>
            <groupId>indi.zhifa.recipegroupId>
            <artifactId>framework-web-allartifactId>
            <exclusions>
                <exclusion>
                    <groupId>indi.zhifa.recipegroupId>
                    <artifactId>framework-mysqlartifactId>
                exclusion>
                <exclusion>
                    <groupId>indi.zhifa.recipegroupId>
                    <artifactId>framework-redisartifactId>
                exclusion>
            exclusions>
        dependency>
        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <scope>providedscope>
        dependency>

        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
        dependency>
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
        dependency>

    dependencies>

三、common模块

3.1 common目标

yml配置读取,通用返回值,通用异常,fastjson,通用工具类
目录结构如图:
芝法酱躺平攻略(1)——SpringBoot 多模块 maven工程搭建_第3张图片

3.2 config包

3.2.1 fastjson

我通常喜欢使用fastjson作为SpringBoot的序列化工具,所以需要加一个配置
其中要注意的是,Long类型通常用作id,会超出前端JavaScript的存储范围,所以要以字符串方式显示
国内的时间,比较喜欢使用老版的"yyyy-MM-dd HH:mm:ss"
目前代码如下:

@Configuration
public class FastJsonHttpMessageConverterConfig {
    @Bean
    public FastJsonConfig getDefaultFastJsonConfig(){
        FastJsonConfig config = new FastJsonConfig();
        config.setWriterFeatures(
                // 保留 Map 空的字段
                JSONWriter.Feature.WriteMapNullValue,
                // 将 String 类型的 null 转成""
                JSONWriter.Feature.WriteNullStringAsEmpty,
                // 将 Number 类型的 null 转成 0
                //SerializerFeature.WriteNullNumberAsZero,
                // 将 List 类型的 null 转成 []
                JSONWriter.Feature.WriteNullListAsEmpty,
                // 将 Boolean 类型的 null 转成 false
                JSONWriter.Feature.WriteNullBooleanAsFalse,
                // 漂亮的输出,这里是学习用,正式工程请注释掉
                JSONWriter.Feature.PrettyFormat,
                // 把Long转化为String
                JSONWriter.Feature.WriteLongAsString,
                // 把BigDecimal转化为String
                JSONWriter.Feature.BrowserCompatible,
                // 枚举
                JSONWriter.Feature.WriteEnumsUsingName);
        config.setDateFormat("yyyy-MM-dd HH:mm:ss");
        config.setWriterFilters();
        return config;
    }

    @Bean
    public FastJsonHttpMessageConverter getFastJsonHttpMessageConverter(FastJsonConfig pFastJsonConfig){

        FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
        converter.setDefaultCharset(StandardCharsets.UTF_8);
        converter.setFastJsonConfig(pFastJsonConfig);
        List<MediaType> mediaTypeList = new ArrayList<>();
        // 解决中文乱码问题,相当于在 Controller 上的 @RequestMapping 中加了个属性 produces = "application/json"
        mediaTypeList.add(MediaType.APPLICATION_JSON);
        mediaTypeList.add(MediaType.TEXT_HTML);
        converter.setSupportedMediaTypes(mediaTypeList);
        return converter;
    }
}

3.2.2 yml配置的读取

如果想在代码中使用独立的yml配置(非application.yml),需要额外增加配置类

@Configuration
public class YamlPropertySourceFactory implements PropertySourceFactory {
    @Override
    public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
        Properties propertiesFromYaml = loadYamlIntoProperties(resource);
        String sourceName = name != null ? name : resource.getResource().getFilename();
        return new PropertiesPropertySource(sourceName, propertiesFromYaml);
    }

    private Properties loadYamlIntoProperties(EncodedResource resource) throws FileNotFoundException {
        try {
            YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
            factory.setResources(resource.getResource());
            factory.afterPropertiesSet();
            return factory.getObject();
        } catch (IllegalStateException e) {
            // for ignoreResourceNotFound
            Throwable cause = e.getCause();
            if (cause instanceof FileNotFoundException) {
                throw (FileNotFoundException) e.getCause();
            }
            throw e;
        }
    }
}

3.3 entity包

3.3.1 RestResponse

通用返回的类

@Data
@Slf4j
@Schema(name = "通用返回")
public class RestResponse<T>{

    @Schema(name = "返回数据")
    private T data;

    @Schema(name = "返回码,200为正常,非200即为错误")
    private int code;

    @Schema(name = "状态信息")
    private String message;

    public RestResponse() {
        code = HttpStatus.OK.value();
    }

    public RestResponse(T pData) {
        code = HttpStatus.OK.value();
        data = pData;
    }
    public RestResponse(int pCode, String errMsg) {
        code = pCode;
        message = errMsg;
    }
    public RestResponse(int pCode, T pData, String errMsg) {
        code = pCode;
        data = pData;
        message = errMsg;
    }
    public static <T> RestResponse<T> builderJsonResult(int code,T data,String msg){
        return new RestResponse<T>(code,data,msg);
    }

    public static <T> RestResponse<T> ok(T pData){
        return new RestResponse<T>(pData);
    }

    public static RestResponse error(String pMsgErr){
        return new RestResponse(500,pMsgErr);
    }
    public static RestResponse error(int pCode, String pMsgErr){
        return new RestResponse(pCode,pMsgErr);
    }

}

3.3.2 ServiceException

通用异常类

public class ServiceException extends RuntimeException {

    @Getter
    private final int code;

    public String getMsg(){
        return getMessage();
    }

    public ServiceException(String msg) {
        super(msg);
        //this.msg = msg;
        this.code = 500;
    }

    public ServiceException(String msg, Throwable e) {
        super(msg, e);
        this.code = 500;
    }

    public ServiceException(int pErrorCode, String msg) {
        super(msg);
        this.code = pErrorCode;
    }
    public ServiceException(int pErrorCode, String msg, Throwable e) {
        super(msg, e);
        this.code = pErrorCode;
    }
}

3.3.3 通用返回的一些annotations

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestController
public @interface ZfRestController {
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OriginalControllerReturnValue {
}

3.4 util包

util包代码暂时不展示了,后面用到再说

四、web-common模块

由于本篇不打算涉及redis和mysql的操作,故只展示web部分的代码

4.1 项目目录

芝法酱躺平攻略(1)——SpringBoot 多模块 maven工程搭建_第4张图片

4.2 swagger相关配置

个人比较喜欢小刀版本的swagger,这个版本的swagger会记录接口的值(即使关闭浏览器)。
这样的话,在测试时会十分方便

SwaggerProperties:

@Configuration
@Data
@ConfigurationProperties(prefix = "swagger")
public class SwaggerProperties {
    /**
     * 是否启用swagger
     */
    private boolean enable = true;
    /**
     * swagger 组名
     */
    private String groupName = "业务接口";
    /**
     * 扫描包配置
     */
    private String apiPackage = "indi.zhifa.**.controller";
    /**
     * 路径正则
     */
    private String apiRegex = "/api/**";
    /**
     * 系统标题
     */
    private String title = "业务接口";
    /**
     * 系统描述
     */
    private String description = "业务接口文档";
    /**
     * 系统版本
     */
    private String version = "1.0.0";
    /**
     * 联系人姓名
     */
    private String name = "芝法酱";
    /**
     * 联系人邮箱
     */
    private String email = "[email protected]";
    /**
     * 联系人地址
     */
    private String url = "https://github.com/hataksumo";
}

Knife4jConfiguration

@Configuration
@EnableOpenApi
public class Knife4jConfiguration {
    public static final String KEY_AUTHORIZATION = "Authorization";
    private final SwaggerProperties mProperties;

    public Knife4jConfiguration(SwaggerProperties pProperties){
        mProperties = pProperties;
    }

    @Bean
    public Docket knife4jDocket() {
        Docket docket=new Docket(DocumentationType.OAS_30)
                .groupName(mProperties.getGroupName())
                .pathMapping("/")
                .enable(mProperties.isEnable())
                .apiInfo(getApiInfo())
                .select()
                .apis(basePackage(mProperties.getApiPackage()))
                .paths(PathSelectors.ant(mProperties.getApiRegex()))
                .build();

        return docket;
    }

    private ApiInfo getApiInfo(){
        return new ApiInfoBuilder()
                .title(mProperties.getTitle())
                .description(mProperties.getDescription())
                .contact(new Contact(mProperties.getName(),mProperties.getUrl(),mProperties.getEmail()))
                .version(mProperties.getVersion())
                .build();
    }

    public static Predicate<RequestHandler> basePackage(final String basePackage) {
        return input -> declaringClass(input).map(handlerPackage(basePackage)).orElse(true);
    }

    private static Function<Class<?>, Boolean> handlerPackage(final String basePackage) {
        return input -> {
            // 循环判断匹配
            for (String strPackage : basePackage.split(";")) {
                boolean isMatch = input.getPackage().getName().startsWith(strPackage);
                if (isMatch) {
                    return true;
                }
            }
            return false;
        };
    }

    private static Optional<? extends Class<?>> declaringClass(RequestHandler input) {
        return Optional.ofNullable(input.declaringClass());
    }
}

4.3 web序列化相关配置

CustomWebMvcConfigurer

@Configuration
@AllArgsConstructor
public class CustomWebMvcConfigurer implements WebMvcConfigurer {

    private final FastJsonHttpMessageConverter fastJsonHttpMessageConverter;

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        Iterator<HttpMessageConverter<?>> it = converters.iterator();
        while (it.hasNext()) {
            HttpMessageConverter<?> converter = it.next();
            if (converter instanceof StringHttpMessageConverter) {
                StringHttpMessageConverter stringHttpMessageConverter = (StringHttpMessageConverter) converter;
                if(!stringHttpMessageConverter.getDefaultCharset().equals(StandardCharsets.UTF_8)){
                    it.remove();
                }
            }
            if (converter instanceof MappingJackson2HttpMessageConverter ||
                    converter instanceof FastJsonHttpMessageConverter) {
                it.remove();
            }
        }
        converters.add(fastJsonHttpMessageConverter);
    }
}

LocalDateTimeSerializerConfig

@Slf4j
@Configuration
public class LocalDateTimeSerializerConfig {
    DateTimeFormatter df;
    @PostConstruct
    protected void init(){
        df = new DateTimeFormatterBuilder()
                .parseCaseInsensitive()
                .append(DateTimeFormatter.ISO_LOCAL_DATE)
                .appendLiteral(' ')
                .append(DateTimeFormatter.ISO_LOCAL_TIME)
                .toFormatter();
    }
    @Bean
    public Converter<String, LocalDateTime> localDateTimeConvert() {
        return new Converter<String, LocalDateTime>() {
            @Override
            public LocalDateTime convert(@NotNull String source) {
                LocalDateTime time = LocalDateTimeUtil.parse(source);
                return time;
            }
        };
    }
}

4.4 全局错误捕捉

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    @ExceptionHandler(JsonParseException.class)
    @ResponseBody
    public RestResponse jsonValidException() {
        return RestResponse.error("Json格式错误!");
    }
    /**
     * Description: 业务主动抛出的异常
     * @author: bixuejun([email protected])
     * @date:  2021/11/27 19:29
     * @param
     * @return
     */
    @ExceptionHandler(ServiceException.class)
    @ResponseBody
    public RestResponse otherException(ServiceException ex) {
        RestResponse result = null;
        if (null != ex){
            log.debug("op=global_exception_handler_log_ServiceException", ex);
            int code = ex.getCode();
            if(code ==0 || code == 500){
                result = RestResponse.error(ex.getMsg());
            }else{
                result = RestResponse.error(code,ex.getMsg());
            }
        }

        return result;
    }
    /**
     * Description: 系统抛出的未知异常
     * @author: bixuejun([email protected])
     * @date:  2021/11/27 19:26
     * @param
     * @return
     */
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public RestResponse handle(Exception e) {
        log.error("op=global_exception_handler_log_unknowError", e);
        return RestResponse.error("发生未知错误 "+e.toString());
    }
}

4.5 全局响应

@RestControllerAdvice(annotations ={ZfRestController.class})
public class GlobalRestfulResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        Annotation originalControllerReturnValue = returnType.getMethodAnnotation(OriginalControllerReturnValue.class);
        if (originalControllerReturnValue != null) {
            return false;
        }
        return true;
    }
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if ( body instanceof RestResponse){
            return body;
        }
        if( body instanceof String){
            return body;
        }
        return RestResponse.ok(body);
    }
}

4.6 DtoUtil

public class DtoEntityUtil {
    static DozerBeanMapper mapper;
    public static void init(){
        mapper = new DozerBeanMapper();
        mapper.setMappingFiles(Collections.singletonList("dozerJdk8Converters.xml"));
    }
    public static <D, E> E trans(D t, Class<E> clazz) {
        if (t == null) {
            return null;
        }
        return mapper.map(t, clazz);
    }
    public static <D, E> List<E> trans(D[] ts, Class<E> clazz) {
        List<E> es = new ArrayList<E>();
        if (ts == null) {
            return es;
        }
        for (D d : ts) {
            E e = (E) trans(d, clazz);
            if (e != null) {
                es.add(e);
            }
        }
        return es;
    }
    public static <D, E> List<E> trans(List<D> ts, Class<E> clazz) {
        List<E> es = new ArrayList<E>();
        if (ts == null) {
            return es;
        }
        for (D d : ts) {
            E e = (E) trans(d, clazz);
            if (e != null) {
                es.add(e);
            }
        }
        return es;
    }
    public static <T> void copy(T dst, T src){
        mapper.map(src, dst);
    }
}

五、业务模块

5.1 本章目标

本次工程,实现启动时读取app节点下的角色,并支持内存级别的角色增删改查。
暴露接口查看color和energy,并支持nacos动态修改

5.2 代码结构

芝法酱躺平攻略(1)——SpringBoot 多模块 maven工程搭建_第5张图片

5.3 配置

5.3.1 配置类

我通常把程序自定义的配置放在app节点下,这样方便管理
使用ConfigurationProperties注解,如图所示

@Data
@Configuration
@ConfigurationProperties(prefix = "app")
public class AppProperty {
    String color;
    int energy;
    List<PlayerProperty> players;
}
@Data
public class PlayerProperty {
    String name;
    String weapon;
    String desc;
}

5.3.2 nacos中的配置

app:
  color: orange
  energy: 7
  players:
    - name: "宵宫"
      weapon: "BOW"
    - name: "影"
      weapon: "POLEARM"
    - name: "心海"
      weapon: "CATALYST"
    - name: "优菈"
      weapon: "CLAYMORE"
    - name: "温迪"
      weapon: "BOW"
    - name: "班尼特"
      weapon: "SWORD"
    - name: "钟离"
      weapon: "POLEARM"
    - name: "北斗"
      weapon: "CLAYMORE"
    - name: "行秋"
      weapon: "SWORD"
swagger:
  enable: true
  group-name: "业务接口"
  api-package: indi.zhifa.recipe.bailan.busy.controller.api
  api-regex: "/api/**"
  title: "业务接口"
  description: "业务接口,提供nacos属性的访问和模拟用户的增删改查"
  version: "1.0.0"
  name: "芝法酱"
  email: "[email protected]"
  url: "https://github.com/hataksumo"

5.4 用到的entity

@Data
public class PlayerDto {
    String name;
    String desc;
    EWeapon weapon;
}
@AllArgsConstructor
public enum EWeapon {
    DEFFAULT(0,"deffault","默认"),
    SWORD(1,"剑","短手剑"),
    POLEARM(2,"长柄","长柄武器"),
    CLAYMORE(3,"大剑","双手剑"),
    BOW(4,"弓","弓"),
    CATALYST(5,"书","法器");

    @EnumValue
    @Getter
    int code;
    @Getter
    String name;
    @Getter
    String desc;
}
@Data
public class PlayerEntity {
    Long id;
    String name;
    String desc;
    EWeapon weapon;
    LocalDateTime createTime;
    LocalDateTime modifyTime;
}

5.5 用户service

通常,我们写service时,会先定义好接口,这样可以帮助我们想清楚要写些什么。
而且可以使代码的文档更加清晰,在别人调用时,不需要看令人头疼的实现,只看接口上的文档即可。

public interface IPlayerService {
    /**
     * 读取yml中初始配置的角色
     * @param pIniPlayers 角色数组
     */
    void init(List<PlayerProperty> pIniPlayers);

    /**
     * 新增角色
     *
     * @param pPlayerDto 角色配置数据
     * @return PlayerEntity 角色实体
     */
    PlayerEntity create(PlayerDto pPlayerDto);

    /**
     * 修改角色
     *
     * @param pId 角色Id
     * @param pPlayerDto 角色信息
     * @return PlayerEntity 角色实体
     */
    PlayerEntity edit(Long pId, PlayerDto pPlayerDto);

    /**
     * 删除角色
     *
     * @param pId 角色Id
     * @return 是否删除成功
     */
    boolean delete(Long pId);

    /**
     * 查找角色信息
     *
     * @param pId 角色Id
     * @return 角色信息
     */
    PlayerEntity info(Long pId);

    /**
     * 查询角色列表
     *
     * @param pName 角色名
     * @param pWeapon 武器
     * @return List 角色列表
     */
    List<PlayerEntity> list(String pName, EWeapon pWeapon);
}

在impl包中写上实现,加上@Component接口后,spring就可以找到响应接口的实现了
由于本篇中没有引用数据库,这里我仅仅做简单的内存实现

@Component
public class PlayerServiceImpl implements IPlayerService {

    Long mId;
    Map<Long,PlayerEntity> mPlayerEntityIdMap;
    Map<String,PlayerEntity> mPlayerEntityNameMap;

    public PlayerServiceImpl(){
        mId = 0L;
        mPlayerEntityIdMap = new HashMap<>();
        mPlayerEntityNameMap = new HashMap<>();
    }

    @Override
    public void init(List<PlayerProperty> pIniPlayers) {
        for(PlayerProperty playerProperty : pIniPlayers){
            PlayerEntity playerEntity = new PlayerEntity();
            playerEntity.setId(getId());
            playerEntity.setCreateTime(LocalDateTime.now());
            playerEntity.setName(playerProperty.getName());
            playerEntity.setWeapon(EWeapon.valueOf(playerProperty.getWeapon()));
            playerEntity.setDesc(playerProperty.getDesc());
            addPlayer(playerEntity);
        }
    }

    @Override
    public PlayerEntity create(PlayerDto pPlayerDto) {
        if(mPlayerEntityNameMap.containsKey(pPlayerDto.getName())){
            throw new ServiceException("已经存在名为"+pPlayerDto.getName()+"的角色");
        }
        PlayerEntity playerEntity = DtoEntityUtil.trans(pPlayerDto,PlayerEntity.class);
        playerEntity.setId(getId());
        playerEntity.setCreateTime(LocalDateTime.now());
        addPlayer(playerEntity);
        return playerEntity;
    }

    @Override
    public PlayerEntity edit(Long pId, PlayerDto pPlayerDto) {
        PlayerEntity playerEntity = checkPlayer(pId);
        DtoEntityUtil.copy(playerEntity,pPlayerDto);
        return playerEntity;
    }

    @Override
    public boolean delete(Long pId) {
        PlayerEntity playerEntity = checkPlayer(pId);
        mPlayerEntityIdMap.remove(playerEntity.getId());
        mPlayerEntityNameMap.remove(playerEntity.getName());
        return true;
    }

    @Override
    public PlayerEntity info(Long pId) {
        PlayerEntity playerEntity = checkPlayer(pId);
        return playerEntity;
    }

    @Override
    public List<PlayerEntity> list(String pName, EWeapon pWeapon) {
        if(StringUtils.hasText(pName)){
            PlayerEntity playerEntity = checkPlayer(pName);
            return Arrays.asList(playerEntity);
        }
        List<PlayerEntity> playerEntityList = new ArrayList<>(mPlayerEntityIdMap.size());
        for(PlayerEntity playerEntity : mPlayerEntityIdMap.values()){
            if(null != pWeapon){
                if(playerEntity.getWeapon() == pWeapon){
                    playerEntityList.add(playerEntity);
                }
            }else{
                playerEntityList.add(playerEntity);
            }
        }
        return playerEntityList;
    }

    protected Long getId(){
        synchronized (this.mId){
            ++mId;
            return mId;
        }
    }

    protected void addPlayer(PlayerEntity pPlayer){
        mPlayerEntityIdMap.put(pPlayer.getId(),pPlayer);
        mPlayerEntityNameMap.put(pPlayer.getName(),pPlayer);
    }

    protected PlayerEntity checkPlayer(Long pId){
        PlayerEntity playerEntity = mPlayerEntityIdMap.get(pId);
        if(null == playerEntity){
            throw new ServiceException("没有找到Id为"+pId+"的角色");
        }
        return playerEntity;
    }

    protected PlayerEntity checkPlayer(String pName){
        PlayerEntity playerEntity = mPlayerEntityNameMap.get(pName);
        if(null == playerEntity){
            throw new ServiceException("没有找到Name为"+pName+"的角色");
        }
        return playerEntity;
    }
}

5.6 controller

5.6.1 SpringBoot服务启动回调

通常,我们希望在服务器启动时执行一段代码。可以在component中使用@PostConstruct,也可以使用一个统一的类,继承自CommandLineRunner来实现。我个人倾向后者,因为正式的项目可能要在启动时做很多事情,放一起方便管理。

@Component
@RequiredArgsConstructor
public class AppInit implements CommandLineRunner {

    private final IPlayerService mPlayerService;
    private final AppProperty mAppProperty;

    @Override
    public void run(String... args) throws Exception {
        DtoEntityUtil.init();
        mPlayerService.init(mAppProperty.getPlayers());
    }
}

5.6.2 NacosPropertyApi

我们写一个接口来测试下nacos的属性自动刷新功能。
大家可以尝试下nacos中配置改变前后,时候接口返回值会跟着变化

@Api(tags = "1.Nacos接口")
@RequestMapping("/api/nacos")
@Slf4j
@ZfRestController
@RequiredArgsConstructor
public class NacosPropertyApi {
    private final AppProperty mAppProperty;

    @Operation(summary = "获取颜色")
    @GetMapping("/color")
    public RestResponse<String> getColor(){
        return RestResponse.ok(mAppProperty.getColor());
    }
    @GetMapping("/energy")
    public int getEnergy(){
        return mAppProperty.getEnergy();
    }
}

5.6.3 用户接口

一般说来,对于一个实体,我们都会写如下接口:
1、新增
PostMapping(“”)
实体VO create(@RequestBody 实体DTO)
2、修改
@PutMapping(“/{id}”)
实体VO create(@PathVariable(“id”) Long pId, @RequestBody 实体DTO)
3、删除
@DeleteMapping(“/{id}”)
RestResponse delete(@PathVariable(“id”) Long pId)
4、获取信息
@GetMapping(“/{id}”)
实体VO info(@PathVariable(“id”) Long pId)
5、分页接口
@GetMapping(“/page”)
Page<实体VO> page(@RequestParam(“current”) int pCurrent,
@RequestParam(“size”) int pSize,
…各查询条件)
由于本例没接入数据库,就是用list替代

@Slf4j
@Api(tags = "2.用户接口")
@RequestMapping("/api/user")
@RequiredArgsConstructor
@ZfRestController
public class PlayerApi {
    private final IPlayerService mPlayerService;

    @Operation(summary = "创建用户")
    @PostMapping("")
    PlayerEntity create(@RequestBody PlayerDto pPlayerDto){
        PlayerEntity playerEntity = mPlayerService.create(pPlayerDto);
        return playerEntity;
    }

    @Operation(summary = "修改用户")
    @PutMapping("/{id}")
    PlayerEntity edit(@PathVariable("id") @Parameter(description = "用户Id") Long pId,
                      @RequestBody PlayerDto pPlayerDto){
        PlayerEntity playerEntity = mPlayerService.edit(pId,pPlayerDto);
        log.info("{}修改为{}",pId, JSON.toJSONString(pPlayerDto));
        return playerEntity;
    }

    @Operation(summary = "删除用户")
    @DeleteMapping("/{id}")
    RestResponse<String> delete(@PathVariable("id") @Parameter(description = "用户Id") Long pId){
        if(mPlayerService.delete(pId)){
            return RestResponse.ok("删除成功");
        }
        return RestResponse.ok("删除失败");
    }

    @Operation(summary = "获取用户信息")
    @GetMapping("/{id}")
    PlayerEntity info(@PathVariable("id") @Parameter(description = "用户Id") Long pId){
        PlayerEntity playerEntity = mPlayerService.info(pId);
        return playerEntity;
    }

    @Operation(summary = "获取用户列表")
    @GetMapping("/list")
    List<PlayerEntity> list(@RequestParam(name = "name",required = false) @Parameter(description = "用户名") String pName,
                            @RequestParam(name = "weapon",required = false) @Parameter(description = "武器") EWeapon pWeapon){
        List<PlayerEntity> playerEntityList = mPlayerService.list(pName,pWeapon);
        return playerEntityList;
    }
}

5.7 测试展示

5.7.1 是用swagger做测试

在实际开发测试中,我们通常是用swagger做测试工具。
相比postman,不需要再手动编辑一遍接口,大大节省时间,提升开发效率
本例是用了加强版的小刀swagger,访问地址是/doc.html

5.7.2 nacos-property

芝法酱躺平攻略(1)——SpringBoot 多模块 maven工程搭建_第6张图片

芝法酱躺平攻略(1)——SpringBoot 多模块 maven工程搭建_第7张图片

5.7.3 业务接口测试

查看弓系职业
芝法酱躺平攻略(1)——SpringBoot 多模块 maven工程搭建_第8张图片
创建早鼬
芝法酱躺平攻略(1)——SpringBoot 多模块 maven工程搭建_第9张图片
查看早鼬
芝法酱躺平攻略(1)——SpringBoot 多模块 maven工程搭建_第10张图片
查找神里凌华
芝法酱躺平攻略(1)——SpringBoot 多模块 maven工程搭建_第11张图片
根据id查早鼬
芝法酱躺平攻略(1)——SpringBoot 多模块 maven工程搭建_第12张图片
修改早鼬介绍
芝法酱躺平攻略(1)——SpringBoot 多模块 maven工程搭建_第13张图片
查看大剑职业
芝法酱躺平攻略(1)——SpringBoot 多模块 maven工程搭建_第14张图片
删除早鼬
芝法酱躺平攻略(1)——SpringBoot 多模块 maven工程搭建_第15张图片

你可能感兴趣的:(java,springboot,spring,boot,maven,java)