JHipster快速开发Web应用

在基于Spring的Web项目开发中,通常存在两个问题:

  1. 普通CRUD的代码基本重复,完全是体力活;
  2. Controller层和持久层之间的数据传递,存在不规范。有人喜欢直接返回JSON,有人喜欢用DTO,有人喜欢直接Entity。

那如何解决这个问题呢?自动生成呗。一群喜欢动脑筋(懒)的人,发明了JHipster。

JHipster是一个基于SpringBoot和Angular的快速Web应用和SpringCloud微服务的脚手架。本文将介绍如何利用JHipster快速开发Web应用。

安装JHipster

JHipster支持好几种安装方式,这里选用最方便的一种方式:Yarn

1. 安装Java8
2. 安装Node.js
3. 安装Yarn
4. 安装JHipster: yarn global add generator-jhipster

创建Web应用

1. 创建项目目录
2. 为一些被墙的资源添加国内源

在项目目录下创建.npmrc文件,为该项目特指一些源。(当然,你也可以为Node和Yarn指定全局的源,那就可以跳过这一步)

1
2
3
4
sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
phantomjs_cdnurl=https://npm.taobao.org/mirrors/phantomjs/
electron_mirror=https://npm.taobao.org/mirrors/electron/
registry=https://registry.npm.taobao.org

3. 在项目目录下运行命令jhipster初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
  (1/16) Which *type* of application would you like to create? (Use arrow keys)
> Monolithic application (recommended for simple projects)
//传统Web应用
Microservice application
//微服务
Microservice gateway
//微服务网关

(2/16) What is the base name of your application? (jhipster) jhipster_quick_start
//输入项目名称,对应Maven的 artifactId

(3/16) What is your default Java package name? (com.chimestone) com.edi
//输入默认包名,对应Maven的 groupId

(4/16) Do you want to use the JHipster Registry to configure, monitor and scale your application? (Use arrow keys)
> No
Yes
//选择是否启用JHipster Registry(微服务默认开启),它可以理解为Eureka、Spring Cloud Config Server、Spring Cloud Admin的一个合体

(5/16) Which *type* of authentication would you like to use? (Use arrow keys)
JWT authentication (stateless, with a token)
> HTTP Session Authentication (stateful, default Spring Security mechanism)
OAuth2 Authentication (stateless, with an OAuth2 server implementation)
//选择认证方式,支持JWT、Session和OATUH2三种

(6/16) Which *type* of database would you like to use? (Use arrow keys)
> SQL (H2, MySQL, MariaDB, PostgreSQL, Oracle, MSSQL)
MongoDB
Cassandra
//选择数据库类型

(7/16) Which *production* database would you like to use? (Use arrow keys)
> MySQL
MariaDB
PostgreSQL
Oracle (Please follow our documentation to use the Oracle proprietary driver)
Microsoft SQL Server
//选择数据库

(8/16) Which *development* database would you like to use?
H2 with disk-based persistence
> H2 with in-memory persistence
MySQL
//选择开发时连接的数据库,这里选H2只是为了演示

(9/16) Do you want to use Hibernate 2nd level cache? (Use arrow keys)
> Yes, with ehcache (local cache, for a single node)
Yes, with HazelCast (distributed cache, for multiple nodes)
[BETA] Yes, with Infinispan (hybrid cache, for multiple nodes)
No
//选择集成到Hibernate2级缓存

(10/16) Would you like to use Maven or Gradle for building the backend? (Use arrow keys)
> Maven
Gradle
//选择打包工具

(11/16) Which other technologies would you like to use?
( ) Social login (Google, Facebook, Twitter)
(*) Search engine using Elasticsearch
>(*) WebSockets using Spring Websocket
( ) API first development using swagger-codegen
( ) [BETA] Asynchronous messages using Apache Kafka
//选择其他的集成框架,这里注意要按下空格键才是启用,启用后会加上*标识。看到无脑自动集成ES是不是泪流满面?

(12/16) Which *Framework* would you like to use for the client? (Use arrow keys)
> Angular 4
AngularJS 1.x
//选择集成的Angular的版本,Angular4采用Webpack打包自动化,而1.x采用Bower和Gulp做自动化

(13/16) Would you like to use the LibSass stylesheet preprocessor for your CSS? (y/N) y
//是否启用LibSass

(14/16) Would you like to enable internationalization support? (Y/n) n
//是否开启国际化

(15/16) Besides JUnit and Karma, which testing frameworks would you like to use? (Press <space> to select, <a> to toggle all, <i> to inverse selection)
>( ) Gatling
( ) Cucumber
( ) Protractor
//选择测试框架,做压力测试的同学有福了

(16/16) Would you like to install other generators from the JHipster Marketplace? (y/N)
//从JHipster市场下载一些其他集成,上下键翻动,空格选取/反选,回车结束。可以看到市场里还是有不少好东西的,像pages服务、ReactNative集成、swagger2markup让你的swagger界面更漂亮、gRPC自动CRUD代码等。

全部选择后,就开始了自动执行生成项目,喝杯水坐等。如果没有翻墙且忘了添加第二步的同学,请坐等卡住。
这里有一点必须提醒,虽然JHipster选项中可以启用ES集成,但受SpringBoot对ES的集成版本限制
JHipster采用的是1.5.X的SpringBoot版本,对应的spring-data-elasticsearch是2.1.X版本,该版本最高支持ES到2.X,醉了~~~具体参见这里
所以如果要使用高版本的ES,还是得用ES自己提供的REST接口,据ES的一篇文章Benchmarking REST client and transport client显示,5.0以后的ES自带的REST接口性能还是可以的。

基本姿势

对于普通Web应用,JHipster在SpringBoot中默认加载了SpringMVCSpringDataSpringJPASpringSecurity几个主要的Web相关的家族成员,LogStash作为日志工具,同时引入了ApacheCommons包、SwaggerHikariCP数据库连接池、Jackson等工具。基本上开发一个JavaWeb项目所需的框架都具备了,甚至还引入了Metrics做运维监控。
此外,它还引入两个特殊的组件——LiquibaseMapperStruct

  • Liquibase是一个帮助管理数据库变更的工具
  • MapperStruct用于自动生成Entity和对应DTO之间的映射关系类,在使用DTO时,千万记得要把自动生成的目录加到IDE的项目路径里

国内搞JavaWeb的,大都喜欢使用Mybatis,可惜的是JHipster默认并不提供Mybatis的集成。但是SpringJPA现在已经封装的十分完善,常规的CRUD和分页,在JHipster下,无需写一行代码(是的,你没看错)。
如果确实需要比较复杂的级联查询,JPA也提供了Specification和Sample实现,性能测试下来其实没多大区别,对付普通Web足以。
如果确实不喜欢JPA,好在SpringBoot本身可以同时使用JPA和Mybatis,那么就把复杂级联用Mybatis,普通CRUD用JPA,达到最佳效果。

代码结构
  • Entity
    JHipster自动产生的项目,内置了UserAuthorityPersistentTokenPersistentAuditEvent四个Entity(如果选取的还有其他组件,如OAUTH2等,会有对应的Entity自动生成)。产生的几张表均以jhi_开头。如果启用了ES,那么除了@Entity注解外,你还会看到@Document注解。
    这里值得一提的是,官方并不推荐修改默认的表名,而且如果要更改User的字段,官方推荐使用创建一个子类继承User类,然后在该子类中把User给Map进来,参见这里。但其实完全自己修改,然后更新数据库字段后,用Liquibase diff命令生成changelog。

  • Controller
    JHipster自动生成的Controller暴露出的RESTful接口都是标准的RESTful API风格,国内很多程序员都不在乎这个东西,导致代码风格及其粗狂。

  • Repository
    这一块得益于SpringJPA的强大,一个JpaRepository接口足以满足大多数需求,有些懒人甚至连Controller都懒得写,给Repository接口加上@RepositoryRestResource注解直接暴露RESTful接口出去。

开始表演

熟悉了代码结构后,我们开始用JHipster来做项目了。

  1. 创建JDL文件描述Entity
    JHipster默认提供了以下几种类型及校验关键字:
类型校验备注
Stringrequired, minlength, maxlength, patternJava String类型,默认长度取决于使用的底层技术,JPA默认是255长,可以用validation rules修改到1024
Integerrequired, min, max
Longrequired, min, max
BigDecimalrequired, min, max
Floatrequired, min, max
Doublerequired, min, max
Enumrequired
Booleanrequired
LocalDaterequired对应java.time.LocalDate
Instantrequired对应java.time.Instant类,DB中映射为Timestamp
ZonedDateTimerequired对应java.time.ZonedDateTime类,用于需要提供TimeZone的日期
Blobrequired, minbytes, maxbytes

官方提供了一个在线的JDL Studio,方便撰写JDL。
例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//双斜杠注释会被忽略掉
/** 这种注释会带到生成的代码里去 */
entity Person {
name String required,
sex Sex
}

enum Sex {
MALE, FEMALE
}

entity Country{
countryName String
}

relationship ManyToOne {
Person{country} to Country
}

paginate Person with pagination
paginate Country with infinite-scroll
  1. jhipster import-jdl your-jdl-file.jdl导入Entity。
    中间会提示有conflict,因为像Cache配置、LiquidBase配置等是已存在的,可以覆盖或merge。
    执行完毕后,看到代码已经生成进去了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package com.edi.domain;
import ...
/**
* 这种注释会带到生成的代码里去
*/
@ApiModel(description = "这种注释会带到生成的代码里去")
@Entity
@Table(name = "person")
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@Document(indexName = "person")
public class Person implements Serializable {

private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotNull
@Column(name = "name", nullable = false)
private String name;

@Enumerated(EnumType.STRING)
@Column(name = "sex")
private Sex sex;

@ManyToOne
private Country country;

// jhipster-needle-entity-add-field - Jhipster will add fields here, do not remove
public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public Person name(String name) {
this.name = name;
return this;
}

public void setName(String name) {
this.name = name;
}

public Sex getSex() {
return sex;
}

public Person sex(Sex sex) {
this.sex = sex;
return this;
}

public void setSex(Sex sex) {
this.sex = sex;
}

public Country getCountry() {
return country;
}

public Person country(Country country) {
this.country = country;
return this;
}

public void setCountry(Country country) {
this.country = country;
}
// jhipster-needle-entity-add-getters-setters - Jhipster will add getters and setters here, do not remove
...

看到有段注释带进去了。
再看下Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.edi.web.rest;
import ...
/**
* REST controller for managing Person.
*/
@RestController
@RequestMapping("/api")
public class PersonResource {
...
/**
* GET /people : get all the people.
*
* @param pageable the pagination information
* @return the ResponseEntity with status 200 (OK) and the list of people in body
*/
@GetMapping("/people")
@Timed
public ResponseEntity<List<Person>> getAllPeople(@ApiParam Pageable pageable) {
log.debug("REST request to get a page of People");
Page<Person> page = personRepository.findAll(pageable);
HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, "/api/people");
return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK);
}

/**
* SEARCH /_search/people?query=:query : search for the person corresponding
* to the query.
*
* @param query the query of the person search
* @param pageable the pagination information
* @return the result of the search
*/
@GetMapping("/_search/people")
@Timed
public ResponseEntity<List<Person>> searchPeople(@RequestParam String query, @ApiParam Pageable pageable) {
log.debug("REST request to search for a page of People for query {}", query);
Page<Person> page = personSearchRepository.search(queryStringQuery(query), pageable);
HttpHeaders headers = PaginationUtil.generateSearchPaginationHttpHeaders(query, page, "/api/_search/people");
return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK);
}
}

其他的不一一列举了,这里着重看下上面两个实现,一个是分页返回列表,一个是ES搜索。
分页这里与我们常规有所不同,它是把分页信息通过PaginationUtil.generatePaginationHttpHeaders(page, "/api/people");这里生成到Header里去了,前端需要从Header里取。

自定义修改返回类型(Optional)

好吧,看到上面肯定有同学要说了,我们平时分页都是返回JSON,所有数据都是返回JSON!
如果非得这么做,那就只能自己做个ResponseUtil,把结果包装成如下格式

1
2
3
4
5
6
7
8
9
10
11
{
"success": true,
"data":{
"content": [{
"name": "张三",
"country": "中国",
"sex": "MALE"
}]
},
"code": 200
}

只需增加两个新类:
CommonResponse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class CommonResponse<T> {

public static final int DEFAULT_CODE = 200;

private boolean success;
private T data;
private int code=DEFAULT_CODE;

public CommonResponse() {
}

public CommonResponse(boolean success, T data) {
this.success = success;
this.data = data;
}

public CommonResponse(boolean success, T data, int code) {
this.success = success;
this.data = data;
this.code = code;
}

... //get & set
}

ResponseUtil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class ResponseUtil {

private static final Logger LOGGER = LoggerFactory.getLogger(ResponseUtil.class);

private ResponseUtil() {
}

public static ResponseEntity<CommonResponse> okResponse(){
return wrapResponse(true, null, DEFAULT_CODE);
}

public static <T> ResponseEntity<CommonResponse> wrapResponse(int statusCode){
return wrapResponse(true, null, statusCode);
}

public static <T> ResponseEntity<CommonResponse> wrapResponse(T data){
return wrapResponse(true, data, DEFAULT_CODE);
}

public static <T> ResponseEntity<CommonResponse> wrapResponse(T data, Pageable pageable){
return wrapResponse(true, data, DEFAULT_CODE);
}

public static <T> ResponseEntity<CommonResponse> wrapResponse(T data, int statusCode){
return wrapResponse(true, data, statusCode);
}

public static <T> ResponseEntity<CommonResponse> wrapResponse(boolean successful,T data){
return wrapResponse(true, data, DEFAULT_CODE);
}

public static <T> ResponseEntity<CommonResponse> wrapResponse(boolean successful, int statusCode){
return wrapResponse(true, null, statusCode);
}

public static <T> ResponseEntity<CommonResponse> wrapResponse(boolean successful, T data, int statusCode){
return ResponseEntity.ok(new CommonResponse<>(successful, data, statusCode));
}

public static <T> ResponseEntity<CommonResponse> wrapResponse(boolean successful,
Optional<T> maybeResponse,
HttpHeaders headers,
int statusCode){
return (ResponseEntity)maybeResponse.map((response) -> {
CommonResponse<T> commonResponse = new CommonResponse<>(successful, response, statusCode);
return ((ResponseEntity.BodyBuilder)ResponseEntity.ok().headers(headers)).body(commonResponse);
}).orElse(new ResponseEntity(new CommonResponse<>(successful, null, HttpStatus.NOT_FOUND.value()), HttpStatus.NOT_FOUND));
}

public static <T> ResponseEntity<CommonResponse> wrapOrNotFound(Optional<T> maybeResponse){
return wrapResponse(true, maybeResponse, null, DEFAULT_CODE);
}
}

然后修改下Controller里面的返回为如下即可

1
2
3
4
5
6
7
8
9
@GetMapping("/people")
@Timed
public ResponseEntity<List<Person>> getAllPeople(@ApiParam Pageable pageable) {
log.debug("REST request to get a page of People");
Page<Person> page = personRepository.findAll(pageable);
//HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, "/api/people");
//return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK);
return ResponseUtil.wrapResponse(page);
}

打包运行

先执行yarn install && bower install (Angular 1.x版本) 或 yarn install(Angular 4版本)对前端代码进行编译。
然后可选:

  • 命令行运行 ./mvnw
  • 带LiveReload前端调试 gulp (Angular1.x版本)或yarn start(Angular 4版本)
  • 生产编译 ./mvnw clean package -Pprod

启动后,默认在本地8080端口启动JHipster的页面,看到已经用它自己的模板实现了常规页面。我们需要做的只是自己做套Angluar页面,套用该模板下的请求处理就好了。

高级姿势

Docker集成

在/src/main/docker/目录下,JHipster提供了docker化所需的所有文件,所以开箱即用。例如,

  • 启动一个mysql数据库: docker-compose -f src/main/docker/mysql.yml up -d
  • 停止并删除该mysql数据库: docker-compose -f src/main/docker/mysql.yml down
  • Maven将本项目打包成docker镜像: ./mvnw package -Pprod dockerfile:build
  • 启动项目容器: docker-compose -f src/main/docker/app.yml up -d

如果需要maven打包docker镜像后推到Registry,则需要修改pom.xml,将dockerfile-maven-plugin中注释掉的一段给打开。

CI集成

(留坑)

结束语

正常情况下,用Jhipster快速实现普通的JavaWeb项目其实仅需三步:1.初始化项目;2.用JDL创建自己的Entity;3.导入JDL;
作为一个脚手架,使用起来已经非常方便了,而且它还支持微服务项目。
既然谈到脚手架,不由自主的会与JFinal等其他脚手架对比,JHipster不一定比其他脚手架轻快,但好在代码规范,Spring家族全套,回头看看,确实可以解决文初的那两个问题。

本文由 EdisonXu - 徐焱飞 创作,采用 CC BY 4.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。本位链接为http://edisonxu.com/2018/02/01/jhipster-quick-start.html
如果您觉得文章不错,可以请我喝一杯咖啡!