用 Spring Boot 实现电商系统 Web API (二)创建多模块项目
大型项目,需要将代码按不同功能,分成不同模块,这样比较好管理和阅读代码,也有助于多人协作。
一、项目结构
1.1 模块说明
项目分成5个模块,分别如下:
模块名称 | 说明 |
webapi | HTTP接口层,对外提供 restful api |
service | 服务层 |
repo | 数据访问层 |
common | 公用层 |
util | 工具类层 |
1.2 创建模块
模块分层在实质是以文件目录的形式实现的,新建一个名为“commerce”的目录,然后在里边创建五跟文件夹,名字为“commerce-webapi”“commerce-service”、“commerce-repo”、“commerce-common”、“commerce-util”。在每个模块名前加“commerce-”前缀,是为了更好的和他模块区分,还有个一个是,每个模块都会单独生成一个jar包,加前缀也是为了避免和外部引用的jar包重名。
创建好上述的五个文件夹后,再在“commerce”目录新建两个名为 “settings.gradle”和“build.gradle”的纯文本文件,在每个子目录中都建一个名为“build.gradle”的纯文本文件。
在“setting.gradle”加入如下内容:
rootProject.name='commerce' include 'commerce-util' include 'commerce-common' include 'commerce-repo' include 'commerce-service' include 'commerce-webapi'
第一行“rootProject.name='commerce'”是可选,不加入也可,setting.gradle文件内容主要是为了说明项目中包含哪些模块。所有的“build.gradle”文件都留空白,稍后我们再添加内容。最后,建立的文件和目录如下:
这样,一个多模块的 Gradle 项目就建成了。
1.3 检查模块
在“commerce”目录中执行如下命令
gradle projects
如果显示内容如下,说明我gradle已经认出我们创建的项目了。
二、添加代码
2.1创建代码目录
在每个子模块中,添加用于存放Java代码的目录,目录统一为
src +-main +-java
2.2 包名规则
对于Java包名,我们统一规范为:顶级包名.项目名.模块名,顶级包名可以随意取,比如:hang.commerce.webapi。
2.3 各个模块代码
为了更清晰说明各个模块是如何组织起来的,我们在每个模块都加入代码,然后让他们互相引用。
我们假设要实现这样一个API:通过GET请求接口,接口返回一个用户的信息,用户信息中包含用户名和最后的登录时间。
2.3.1 根目录
在根目录的 build.gradle 文件加入如下内容:
subprojects{ apply plugin:'java' sourceCompatibility=1.8 group='hang.commerce' version='0.0.1' repositories{ jcenter(); mavenCentral() maven { url "https://repo.spring.io/snapshot" } maven { url "https://repo.spring.io/milestone" } } dependencies{ } sourceSets{ main{ resources{ srcDirs=['src/main/resources', 'src/main/java'] } } } tasks.withType(JavaCompile) { options.encoding = 'UTF-8' } }
这里主要是对各个子项目(模块)进行通用配置,避免在每个模块中重复配置。
2.3.2 commerce-repo 模块
添加 User.java作为数据层的实体类
commerce/commerce-repo/src/main/java/hang/commerce/repo/User.java
package hang.commerce.repo; import org.springframework.stereotype.Component; /** * 用户类 */ @Component public class User { /** * 名称 */ private String name; /** * 最后登录时间 */ private String lastLoginTime; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getLastLoginTime() { return lastLoginTime; } public void setLastLoginTime(String lastLoginTime) { this.lastLoginTime = lastLoginTime; } }
在User类上,我使用了“@Component”注解,这样就可以使用Spring Boot的依赖注入(IOC)功能,在使用的地方用“@Autowired”注解来注明要注入的地方。
由于要使用Component这个注解类,所以我们还需要在 commerce-repo 模块中的 build.gradle 加入如下内容以引入JAR包。
commerce/commerce-repo/build.gradle
dependencies { compile "org.springframework:spring-context:5.0.0.RELEASE" }
2.3.3 commerce-util 模块
我们需要格式化用户最后的登录时间,在util模块增加一个时间工具类。
commerce/commerce-util/src/main/java/hang/commerce/util/DateTimeTool.java
package hang.commerce.util; import java.text.SimpleDateFormat; import java.util.Date; /** * 时间工具类 */ public class DateTimeTool { /** * 日期格式化 * @param time 日期 * @return yyyy-MM-dd HH:mm:ss 格式的日期 */ public static String format(Date time){ return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(time); } }
commerce-util 模块的 build.gradle 文件不需要增加任何内容。
2.3.4 commerce-service 模块
服务模块从 repo模块取数据,然后给webapi模块提供数据。在本示例中,我们只是简单提供一个 get() 方法,返回User对象。为了指明这个是一个服务,我们在 UserService 类上加了 “@Service” 注解,这样Spring会自动完成依赖注入,在需要 Service 的地方,通过 “@Autowired” 注解注入 ,比如,我们需要User对象时,不是直接 new 出来,而是通过 Autowired 注入。代码中,设置用户的最后登录时间时,用到了 “commerce-util”的 “DateTimeTool”工具类中的方法。
commerce/commerce-service/src/main/java/hang/commerce/service/UserService.java
package hang.commerce.service; import hang.commerce.repo.User; import hang.commerce.util.DateTimeTool; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Date; @Service public class UserService { @Autowired private User user; public User get(){ user.setName("Hello"); user.setLastLoginTime(DateTimeTool.format(new Date())); return user; } }
在 service 层,我们就使用到了 gradle 的模块间引用功能。gradle 模块间的引用只需要一行代码就可以实现,
commpile project(':模块名')
模块名称前的“:”是分隔符,相当于Linux系统中的文件路径中 “/”,第一个“:”表示根目录。
由于“commerce-service”模块使用到了“commerce-repo”模块和“commmerce-util”模块中的代码,所以,我们需要在 “build.gradle”加入依赖。
对外部Jar包的依赖,写法如下
compile "org.springframework:spring-context:5.0.0.RELEASE"
规则是 compile “组织名称:包名:版本号”,gradle 会自动根据 build.gradle 文件中的 repositories{} 的配置到网上下载Jar包,缓存到本地。
在 commerce-service 模块中,buidl.gradle 的写法如下:
commerce/commerce-service/build.gradle
dependencies { compile project(':commerce-repo') compile project(':commerce-util') compile "org.springframework:spring-context:5.0.0.RELEASE" }
2.3.5 commerce-common 模块
为了规范API的返回值格式,我们在 commerce-common 模块增加了API返回值类
commerce/commerce-common/src/main/java/hang/commerce/common/ApiResult.java
package hang.commerce.common; /** * API返回值对象 * @param <T> 返回的数据类型 */ public class ApiResult<T> { /** * 状态码 */ private ApiResultCode code; /** * 返回的消息字符串 */ private String message; /** * 返回的数据 */ private T data; /** * 异常名称 */ private String exception; /** * 访问路径 */ private String path; public ApiResultCode getCode() { return code; } public void setCode(ApiResultCode code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public T getData() { return data; } public void setData(T data) { this.data = data; } public String getException() { return exception; } public void setException(String exception) { this.exception = exception; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } }
commerce/commerce-common/src/main/java/hang/commerce/common/ApiResultCode.java
package hang.commerce.common; /** * API 返回值状态码 */ public enum ApiResultCode { /** * 成功 */ OK, /** * 失败 */ Failed, /** * 未找到资源 */ NotFound, /** * 未授权 */ NotAuth }
由于 commerce-common 模块没有使用其他模块或者外部的Jar包,所以 commerce-common模块的 build.gradle 文件不需要增加任何内容。
2.3.6 commerce-webapi 模块
commerce-webapi 模块是入口模块,需要配置 spring boot。由于我们使用了Spring Boot的依赖注入功能,所以我们需要在入口类上加上注解指明要IOC要扫描的包范围,如下
@ComponentScan(value = "hang.commerce") // 依赖注入扫描
commerce/commerce-webapi/src/main/java/hang/commerce/webapi/WebApiApplication.java
package hang.commerce.webapi; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan; /** * spring boot 应用入口 */ @ComponentScan(value = "hang.commerce") // 依赖注入扫描 @SpringBootApplication public class WebApiApplication { public static void main(String[] args){ SpringApplication.run(WebApiApplication.class, args); } }
我们将所有的API统一放到 controller 包中。
使用
@RestController
指明,这是 restfule api。
使用
@RequestMapping("api/home")
指明 API的前缀。
在 Controller中,需要使用到服务层的服务时,只需要写如下代码即可,Spring Boot IOC 会自动帮我们处理余下的事情。
@Autowired private UserService userService;
controller的代码如下:
commerce/commerce-webapi/src/main/java/hang/commerce/webapi/controller/HomeController.java
package hang.commerce.webapi.controller; import hang.commerce.common.ApiResult; import hang.commerce.common.ApiResultCode; import hang.commerce.repo.User; import hang.commerce.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RequestMapping("api/home") @RestController public class HomeController { @Autowired private UserService userService; @GetMapping("/") public ApiResult<User> get(){ User user = userService.get(); ApiResult<User> result = new ApiResult<>(); result.setCode(ApiResultCode.OK); result.setMessage("获取成功"); result.setData(user); return result; } }
在commerce-webapi模块中的 build.gradle 和正常的Spring Boot程序配置无异,只是增加了对项目中其他模块的引用
compile project(':commerce-common') compile project(':commerce-util') compile project(':commerce-repo') compile project(':commerce-service')
最后,build.gradle 的配置如下
commerce/commerce-webapi/build.gradle
buildscript { ext { springBootVersion = '2.0.0.M5' } repositories { mavenCentral() maven { url "https://repo.spring.io/snapshot" } maven { url "https://repo.spring.io/milestone" } } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } } group = 'hang.commerce' version = '0.0.1' sourceCompatibility = 1.8 apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' repositories { mavenCentral() maven { url "https://repo.spring.io/snapshot" } maven { url "https://repo.spring.io/milestone" } } dependencies{ compile project(':commerce-common') compile project(':commerce-util') compile project(':commerce-repo') compile project(':commerce-service') compile('org.springframework.boot:spring-boot-starter-web') }
至此,整个项目的代码和配置已经全部完成。
三、编译运行
3.1 编译
在 commerce 目录中执行
gradle build
即可进行编译。
3.2 运行
在 commerce 目录中执行
gradle bootRun
即可运行整个项目。
四、gradle wrapper
如果别人的电脑上没装有gradle,那么就无法编译和运行我们的程序,为了解决这个问题,我们可以把gradle附带到代码中。
在 commerce 目录中执行
gradle wrapper
之后,gradle会在目录中加入
commerce +--gradlew +--gradlew.bat +--gradle +--wrapper +--gradle-wrapper.jar +--gradle-wrapper.properties
这样,当别人拿到我们的代码后,不需要安装 gradle 程序,只需要把之前的命令“gradle”改成 “gradlew”就可以了,比如
gradlew build
当执行 “gradlew”命令时,会根据 gradle/wrapper/gradlw-wrapper.properties文件中的配置,到网上下载 gradle 程序,存放在 用户目录/.gradle/wrapper/dists/gradle-x.x.x-bin 目录,比如
distributionUrl=https\://services.gradle.org/distributions/gradle-4.2.1-bin.zip
会到网上下载 gradle-4.2.1-bin.zip 存放到 用户目录/.gradle/wrapper/dists/gradle-4.2.1-bin/随机字符串/gradle-4.2.1/
五、总结
1、在根目录中加入 settings.gradle 的文本文件,文件内容写明子模块(子文件夹名),比如
include 'commerce-util' include 'commerce-common' include 'commerce-repo' include 'commerce-service' include 'commerce-webapi'
2、根目录加入 build.gradle 的文本文件,作为项目和各个子模块的通用配置。
3、每个子模块都加入 build.gradle 的文本文件,作为子模块的配置。
4、各个模块间的引用通过在 dependencies{} 中加入 compile project(“:子模块名”),比如:
dependencies{ compile project(':commerce-common') compile project(':commerce-util') compile project(':commerce-repo') compile project(':commerce-service') compile('org.springframework.boot:spring-boot-starter-web') }
六、附录
源码 https://github.com/jinghang/commerce/tree/master/0x02/commerce