项目结构介绍

多项目微服务的场景下项目结构是非常重要的, 如果项目结构设置的不好会导致以下问题:

  • 多个项目直接的依赖混乱,难以管理
  • 多依赖了许多不必要的第三方包,导致服务过重,资源浪费
  • 当我们的项目功能需要给其它系统(内部其他系统或者第三方开发团队)使用时,难以让别人直接依赖我们系统的jar

Maven VS Gradle

推荐使用maven管理项目依赖,虽然使用Gradle管理依赖更加简介和灵活,但是maven对比Gradle有以下优点:

  • Maven比Gradle简单: maven使用xml配置项目Gradle使用Groovy配置,xml大家都很熟,Groovy有学习成本
  • Maven比Gradle兼容性好: Maven的新版本都能完全兼容之前的项目配置,Groovy新版本的API变化很大会导致之前的配置会完全不可用(报错),对新的API网上资料很少只能上官网查看学习新API成本也高。相反Maven的各种场景配置网上都要很多资料
  • Maven比Gradle轻量: Gradle构建的时候很消耗CPU和内存,Maven则相对好很多
  • Maven比Gradle可控: Gradle本地仓库储存的jar位置难找人为干预很难,Maven本地仓库存储的Jar可以人为修改模拟
  • 开发工具对Maven的支持比Gradle好: IntelliJ IDEA的Maven插件比Gradle好用

我认为一个依赖管理工具(或者说项目管理工具)不需要太多的灵活性,够用就行,使用越简单越好。就这几点而言Maven优于Gradle。当然我不是说Gradle不优秀,相反Gradle非常优秀,非常之强大,但是对于做业务系统的初中级开发人员来讲他不适合,因为他比Maven更难掌握和把控。

Maven多模块项目结构

└── xxx                 # Maven父模块
    ├── xxx-model       # 项目model模块
    ├── xxx-server      # 项目server模块
    ├── xxx-client      # 项目client模块(可选)
    ├── xxx-...         # 项目其他模块(可选)

各个模块功能职责说明:

  • xxx(项目父模块): 项目父模块仅仅只是一个Maven parent项目,它只有一个pom.xml文件,没有其他任何代码,项目父模块的职责如下:
    • 定义项目的groupIdartifactIdversion
    • 管理全局第三方的依赖以及其版本(这样子模块就不要写明依赖的版本号)
    • 管理各个子模块的版本(统一各个子模块版本号)
    • Maven全局统一配置(编译、打包、部署、发布以及所有模块公用依赖等等…)
  • model模块: 项目中用到的POJO都放到这个模块,主要包括:Entity(数据库实体类)、(DTO)请求相应传输对象、BO(业务对象)、Model(数据模型)等等。model模块必须遵守以下规则:
    • model模块不能依赖其他模块,三方依赖越少越好
    • model模块的职责就是管理整个服务的POJO类型,不能有业务代码
    • model模块的目的就是在有需要是被其它系统依赖
  • server模块: 可以直接打包成一个可以直接运行的SpringBoot Jar包,也是这整个项目最后需要部署的模块,server模块模块必须遵守以下规则:
    • 一般而言server模块需要依赖这个项目中的其它模块(model模块、client模块、…)
    • 服务中的所有业务代码都写在server模块不能中(server模块的职责就是完成系统业务)
    • server模块不能被其它系统依赖,所以可以谁心所欲的依赖第三方jar(当然是项目中需要用到的)
  • client模块: 这是一个可选模块,当我们需要向其他系统暴露我们的服务的时候(以RPC或RESTful方式)就需要这个模块,这个client模块必须遵守以下规则:
    • 必须依赖model模块,除了依赖一些RPC或RESTful远程调用框架之外最好不要有其他第三方的依赖
    • client模块的职责就是为其它系统提供调用当前服务的客户端API,所以client模块的依赖越少越好。client模块也不能有业务代码,只能声明服务调用的接口和一些工具类

各个模块的依赖说明:(虚线表示依赖)

依赖说明

项目结构约定

上面说明了Maven各个项目模块的职责功能说明和一些约束,你应该对这样分模块的用意有了一个大致的了解。这里我给出更加详细和具体的项目结构(包括package的结构),更加明确了一个类应该放在哪个package下。对于Maven多模块的项目结构的详细配置可以参考这里

基于Maven的项目结构

首先是Maven的多模块结构(上面已经给出了),如下:

├── xxx                 # Maven父模块
│   ├── xxx-model       # 项目model模块
│   │   └── pom.xml
│   ├──xxx-client       # 项目client模块(可选)
│   │   └── pom.xml
│   └── xxx-server      # 项目server模块
│       └── pom.xml
└── pom.xml

假设我们的项目名称叫xxx,当前公司或组织机构名称为[company name],那么我们的package基础路径就是com.[company name].xxx,接下来我分别讲解每个模块的具体package的结构

model模块包结构

├──xxx-model
    ├── src
    │   ├── main
    │       ├── java
    │       │   └── com.[company name].xxx                  # 包基础路径
    │       │                          ├── entity           # 与数据库一一对应的实体类POJO
    │       │                          ├── model            # 其它可公用POJO类
    │       │                          ├── dto              # Controller层请求数据或者响应数据DTO包
    │       │                          │   ├── request      # Controller层请求数据DTO实体
    │       │                          │   ├── response     # Controller层响应数据DTO实体
    │       │                          ├── ...              # 其他自定义package
    │       └── resources                                   #
    │           ├── ...                                     #
    └── pom.xml                                             # Maven配置文件

所有POJO类的命名规则:

  • entity: 必须与对应的数据库表名称要一致(使用下划线转驼峰)
  • dto.request: 必须以Req结尾
  • dto.response: 必须以Res结尾
  • model: 可公用POJO类,所有含义相同的成员变量名称必须跟其它的POJO类一致
  • 所有POJO类必须实现Serializable接口,含义相同的成员变量名称必须一致
  • 所有POJO类的成员变量必须使用包装类型(不能使用intboolean),而且最好不要有默认值
  • 所有entity的成员变量必须不能有默认值(如果有默认值使用ORM的时候会有坑)
  • 当遇到statustype之类的字段名时,最好不要使用枚举类型(有坑),建议使用数值常量(int),热常量统一定义在Constant.java文件里面

server模块包结构

├── xxx-server
    ├── src
    │   ├── main
    │   │   ├── java
    │   │   │   └── com.[company name].xxx                      # 包基础路径
    │   │   │                          ├── config               # SpringBoot项目配置包
    │   │   │                          ├── controller           # Controller包-MVC功能
    │   │   │                          ├── service              # Service包-完成业务逻辑的包
    │   │   │                          ├── mapper               # Mapper包(也称为DAO层)-提供与关系型数据库交换功能
    │   │   │                          ├── utils                # 工具包-系统工具类
    │   │   │                          ├── ...                  # 其它自定义包
    │   │   │                          ├── StartApp.java        # SpringBoot项目启动类
    │   │   │                          └── Swagger2Config.java  # Swagger2接口文档配置类
    │   │   │
    │   │   └── resources                                       # 项目资源文件包(配置文件、前端页面文件、系统设计文档等)
    │   │       ├── database                                    # 数据库设计文档包
    │   │       │   ├── xxx_MySql.sql                           # 表结构设计脚本文件
    │   │       │   ├── ...
    │   │       ├── static                                      # 前端静态资源包
    │   │       ├── dev                                         # 开发环境配置包
    │   │       │   ├── application.yml                         # SpringBoot配置文件
    │   │       │   └── logback.xml                             # logback日志配置文件
    │   │       ├── prod                                        # 生产环境配置包
    │   │       ├── ...                                         # 其他自定义包
    │   └── test
    │       ├── java                                            # 项目单元测试包
    │       │   ├── ...
    │       └── resources                                       # 项目单元测试资源文件
    │           ├── ...
    └── pom.xml                                                 # Maven配置文件

server模块class命名规则:

  • config: 对需要使用@Bean注入的实例统一放在BeanConfiguration.java文件里。对于项目自定义的配置必须使用@ConfigurationProperties注解声明注入,对应的POJO必须使用GlobalConfig.java命名,不可使用@Value注解
  • controller: 必须用Controller结尾
  • service: 必须使用Service结尾。出特殊情况以外(如:通过dubbo暴露服务),禁止定义一个service接口然后实现,造成多余的不必要的接口代码
  • mapper: 必须使用Mapper结尾,如果是某张表的mapper,必须使用对于的实体类+Mapper形式命名。MyBatis对用的mapper.xml文件命名必须与mapper接口名一致,而且也要放在这个目录下

多环境配置文件说明:
通常一个项目要经过“开发 -> 测试 -> 发版”这几个过程,而每个环境的配置(如:数据库配置)可能不一样,所以不同的环境需要不同的配置文件,虽然SpringBoot框架有这个功能,但是我觉得SpringBoot的方案把所有的配置文件放在一个包下开发时手动修改和查看时容易搞错导致问题,我已我建议不同环境的配置文件使用不同的包存放,通过Maven打包的功能来使用不同的配置文件。

下面是Maven多环境打包的配置:

<profiles>
    <profile>
        <id>dev</id>
        <properties>
            <env>${basedir}/src/main/resources/dev</env>
        </properties>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
    </profile>
    <profile>
        <id>prod</id>
        <properties>
            <env>${basedir}/src/main/resources/prod</env>
        </properties>
    </profile>
</profiles>

<build>
    <!--项目结构-->
    <sourceDirectory>src/main/java</sourceDirectory>
    <testSourceDirectory>src/test/java</testSourceDirectory>
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
        </resource>
        <resource>
            <directory>${env}</directory>
            <filtering>true</filtering>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
                <include>**/*.yml</include>
            </includes>
            <excludes>
                <exclude>dev/**</exclude>
                <exclude>prod/**</exclude>
            </excludes>
        </resource>
        <resource>
            <directory>src/main/resources</directory>
            <filtering>false</filtering>
            <includes>
                <include>static/**</include>
            </includes>
        </resource>
    </resources>
    <testResources>
        <testResource>
            <directory>src/test/resources</directory>
        </testResource>
    </testResources>
</build>

client模块包结构

├── xxx-client
    ├── src
    │   ├── main
    │       ├── java
    │       │   └── com.[company name].xxx              # 基础包路径
    │       │                          ├── client       # 调用服务端API的client类
    │       │                          ├── ...          # 其他自定义package
    │       └── resources
    │           ├── ...
    └── pom.xml                                         # Maven配置文件

这个模块是可选的,在项目初期可能不需要这个模块,因为项目内的Service不需要对外系统直接提供服务。所有这个模块可以在后期需要的时候加上。

多业务模块分包说明

在一个复杂业务的系统里大多会有多个业务模块比如vip(会员)order(订单)模块,常用的分包主要有一下两种模式:

按照系统组件职责分包(推荐)

├── controller   # Controller层
│   ├── vip      # [会员] - Controller层
│   ├── order    # [订单] - Controller层#
├── service      # Service层
│   ├── vip      # [会员] - Service层
│   ├── service  # [订单] - Service层#
├── mapper       # Mapper层
│   ├── vip      # [会员] - Mapper层
│   ├── service  # [订单] - Mapper层

按照业务模块分包(不推荐)

├── vip              # [会员模块]
│   ├── controller   # [会员] - Controller层
│   ├── service      # [会员] - Service层
│   ├── mapper       # [会员] - Mapper层
│
├── order            # [订单]
│   ├── controller   # [订单] - Controller层
│   ├── service      # [订单] - Service层
│   ├── mapper       # [订单] - Mapper层

推荐使用按照系统组件职责分包的原因: 因为系统中的各个业务模块也会有相互依赖的关系,比如用户下订单的时候需要判断此用户是不是VIP会员,此时order模块就会依赖vip模块,在代码层面就是OrderService依赖VipService如果使用按照业务模块分包会出现这两个class文件跳出了两层包进行依赖关联不太好,而且使用IDE查看代码时左边的目录树就会跳来跳去不是很方。而使用按照系统组件职责分包就不会有这些问题,而且我们的Maven模块和其他包设计都是按照系统组件职责分包的,风格也是统一的!

代码示例

启动类(StartApp.java)

/**
 * 应用启动类
 * Created by lzw on 2017/2/25.
 */
@Slf4j
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"com.[company name].xxx.client"})
@SpringBootApplication(scanBasePackages = {"com.[company name].xxx"})
public class StartApp {

    public static void main(String[] args) {
        TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
        ApplicationContext ctx = SpringApplication.run(StartApp.class, args);
        log.info("### 服务启动完成 === " + ctx);
    }
}

Swagger2Config.java

/**
 * Swagger2配置
 * <p>
 * 作者:lizw <br/>
 * 创建时间:2017/6/12 9:28 <br/>
 */
@Profile({"dev", "test"})
@Configuration
@EnableSwagger2
public class Swagger2Config {

    @Bean
    public Docket createApi() {
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("xxx-server")
                // .description("description")
                // .termsOfServiceUrl("termsOfServiceUrl")
                .version("0.0.1-SNAPSHOT")
                // .license("license")
                // .licenseUrl("licenseUrl")
                // .termsOfServiceUrl("termsOfServiceUrl")
                // .contact(contact)
                // .extensions()
                .build();
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo)
                .groupName("xxx-server")
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.[company name].xxx.controller"))
                .paths(PathSelectors.any())
                .build();
    }
}

SQL脚本示例(DDL)

/* ====================================================================================================================
    user -- 用户表
==================================================================================================================== */
create table user
(
    id              bigint          not null        auto_increment                          comment '主键id',
    username        varchar(63)     not null        unique                                  comment '登录名',
    password        varchar(127)                                                            comment '密码',
    user_type       int(1)          not null        default 0                               comment '用户类型,0:系统内建,1:外部系统用户',
    telephone       varchar(31)                     unique                                  comment '手机号',
    email           varchar(63)                     unique                                  comment '邮箱',
    expired_time    datetime(3)                                                             comment '帐号过期时间',
    locked          int(1)          not null        default 0                               comment '帐号是否锁定,0:未锁定;1:锁定',
    enabled         int(1)          not null        default 1                               comment '是否启用,0:禁用;1:启用',
    description     varchar(511)                                                            comment '说明',
    create_at       datetime(3)     not null        default current_timestamp(3)            comment '创建时间',
    update_at       datetime(3)                     on update current_timestamp(3)          comment '更新时间',
    primary key (id)
) comment = '用户表';
create index user_username on user (username);
create index user_telephone on user (telephone);
create index user_email on user (email);
/*------------------------------------------------------------------------------------------------------------------------

--------------------------------------------------------------------------------------------------------------------------*/
文档更新时间: 2020-03-13 16:30   作者:lizw