打算就”用Spring Boot & Cloud和Angular2快速搭建微服务web应用“这个题目写一系列文章,作为自己学习的一个记录。在参加完读脉组织的一个Java培训活动后,发现自己在这方面的知识已经落后好多年了。讲师Josh的激情演讲,也让我看到了自己和顶级程序员的差距。在此感谢读脉组织的这次活动,感谢Josh的激情演讲,给了我写这个系列文章的动力。

读脉:http://readmore.cc/

Josh:https://github.com/joshlong


一切从“Spring Initializr”开始

为了快速地搭建和运行Spring Boot项目,Pivotal提供了称之为“Spring Initializr”的web界面,用于下载预先定义好的Maven或Gradle构建配置。为实现RESTful CRUD,访问 http://start.spring.io/,创建一个新的项目叫做user-service,为health-travel提供用户管理服务,并且选择如下依赖:Rest Repositories,JPA,MySQL。然后点击“Generate Project”会产生一个名为user-service的zip包,里面包含了项目需要的Maven工程文件。


打开pom.xml,可以看到项目的依赖已经生成好了:
<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>

	<groupId>com.healtrav</groupId>
	<artifactId>user-service</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>user-service</name>
	<description>user management service for healtrav</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.4.0.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-rest</artifactId>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>


</project>

同时注意到以下几个依赖:

  • spring-boot-starter-data-jpa:使用Spring Data JPA 读写数据。Spring Data JPA是Spring Data项目的一部分,用于简化持久层的业务逻辑。它会根据方法的名字来确定方法要实现什么样的业务逻辑,所以developer只要按照规范的名字去命名方法,声明接口。参考链接:http://projects.spring.io/spring-data-jpa/
  • spring-boot-starter-data-rest:用RESTful的方式访问Spring Data数据仓库,提供了CRUD。参考链接:http://projects.spring.io/spring-data-rest/
Spring Initializr还生成了user-service的主程序文件UserServiceApplication.java
package com.healtrav;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class UserServiceApplication {

	public static void main(String[] args) {
		SpringApplication.run(UserServiceApplication.class, args);
	}
}
现在我们只需要添加一些业务代码,就可以直接实现一个Restful,支持CRUD的web应用。

实现

添加domain/User.java
package com.healtrav.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.validation.constraints.NotNull;

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @NotNull
    private String username;

    @NotNull
    private String password;

    public long getId() {
        return id;
    }

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

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

}

User对应数据库的user表。

添加repository/UserRepository.java
package com.healtrav.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

import com.healtrav.domain.User;

@RepositoryRestResource(collectionResourceRel = "user", path = "user")
public interface UserRepository extends JpaRepository<User, Long> {

    User findByUsername(@Param("username") String username);
}
JpaRepository的详细信息: JpaRepository。前面提到过Spring Data JPA的命名规范,findByUsername就是利用了这样的规范,让Spring Data JPA实现按照username查找User。可以通过在方法名中添加Distinct,Asc,Desc,LessThen等来实现去除重复值,升序,降序,小于等的条件的查找,例如findDistinctByUsername 另外JpaRepository提供了大量的方法,从上面的链接可以看到,该接口及其继承的接口,提供了基本的查询,分页,删除,保存,修改,及批量功能。参考:http://docs.spring.io/spring-data/jpa/docs/current/reference/html/

修改resources/application.properties
# MySQL data source settings
spring.datasource.url=jdbc:mysql://localhost:3306/healtrav
spring.datasource.username=root
spring.datasource.password=

spring.datasource.initial-size=20
spring.datasource.max-idle=60
spring.datasource.max-wait=10000
spring.datasource.min-idle=10
spring.datasource.max-active=200

# auto create tables and data for database healtrav
spring.jpa.generate-ddl=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.datasource.schema=..\..\..\db\schema.sql
spring.datasource.data=..\..\..\db\data.sql

# show each sql for debug
spring.jpa.show-sql = true

文件里面的datasource用来配置MySQL数据库连接。Spring Data还支持MongoDB,H2嵌入式数据库等。如果用MongoDB,H2等,只需要在Spring Initializr里面添加依赖,不需要添加datasource配置。另外配置增加了自动运行schema.sql和data.sql的功能,自动创建数据库表和添加数据(healtrav数据库必须已经存在)。show-sql是为了让Spring Data显示每个SQL语句,方便调试。
添加schema.sql
-- MySQL Script generated by MySQL Workbench
-- 09/20/16 18:12:37
-- Model: New Model    Version: 1.0
-- MySQL Workbench Forward Engineering

SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES';

-- -----------------------------------------------------
-- Schema healtrav
-- -----------------------------------------------------
DROP SCHEMA IF EXISTS `healtrav` ;

-- -----------------------------------------------------
-- Schema healtrav
-- -----------------------------------------------------
CREATE SCHEMA IF NOT EXISTS `healtrav` DEFAULT CHARACTER SET utf8 ;
USE `healtrav` ;

-- -----------------------------------------------------
-- Table `healtrav`.`user`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `healtrav`.`user` ;

CREATE TABLE IF NOT EXISTS `healtrav`.`user` (
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(64) NOT NULL,
  `password` VARCHAR(128) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE INDEX `id_UNIQUE` (`id` ASC),
  UNIQUE INDEX `username_UNIQUE` (`username` ASC))
ENGINE = InnoDB;


SET SQL_MODE=@OLD_SQL_MODE;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;
小结
短短的这些代码,已经实现了对库表user的CRUD功能,并且提供了标准的Restful API。文件UserRepository.java中的@RepositoryRestResource(collectionResourceRel = "user", path = "user")注解,在localhost:8080/user下面,生成了Restful API,甚至还有API的接口说明。

验证

Windows版本的git bash提供了很多有用的工具,其中一个就是curl。curl可以用来验证刚才建立的service。
在git bash中进入user-service目录,用mvn运行:./mvnw spring-boot:run
在另一个git bash中,运行命令:
$ curl http://localhost:8080/
会得到当前的Restful URI列表。其中一个是"href" : "http://localhost:8080/user{?page,size,sort}",

运行命令:
$ curl http://localhost:8080/user
会得到user路径下面的URI列表,及一个user数组。但是现在这个user数组是空的,因为我们还没有添加用户信息。

运行命令:
$ curl -i -X POST -H "Content-Type:application/json" -d '{  "username" : "cuiwader",  "password" : "123" }' http://localhost:8080/user
会添加数据到数据库,并且会返回新添加的用户的信息。

运行命令:
$ curl http://localhost:8080/user/search/findByUsername?username=cuiwader
会调用UserRepository.java中,接口UserRepository的findByUsername方法。而且该方法的实现也是Spring Data JPA根据方法的名字自动生成的。

再次运行命令:
$ curl http://localhost:8080/user
会得到user路径下面的URI列表,及一个user数组。这次这个数组多了一个元素,即刚才添加的那个元素,但是请大家仔细看下面的输出,数组元素没有id字段,因为默认id是不导出的。可以通过继承RepositoryRestMvcConfiguration,重写configureRepositoryRestConfiguration(RepositoryRestConfiguration config)方法,并且在方法里面调用
config.exposeIdsFor(class)的方式增加id导出。但是应该使用_links的href去调用后台,而不是自己拼接id。输出如下:
$ curl http://localhost:8080/user-service/user
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   726    0   726    0     0  15782      0 --:--:-- --:--:-- --:--:-- 15782{
  "_embedded" : {
    "user" : [ {
      "username" : "cuiwader",
      "password" : "123",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/user-service/user/1"
        },
        "user" : {
          "href" : "http://localhost:8080/user-service/user/1"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/user-service/user"
    },
    "profile" : {
      "href" : "http://localhost:8080/user-service/profile/user"
    },
    "search" : {
      "href" : "http://localhost:8080/user-service/user/search"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 1,
    "totalPages" : 1,
    "number" : 0
  }
}

解决复杂问题

这里还有一个遗留问题,如果业务逻辑特别复杂,不能通过简单的方法命名规范来实现怎么办呢?一般遇到这样的问题,只需要抱有一个信念:Spring如此灵活,不可能有解决不了的问题。另外自己遇到的问题,别人也一定遇到过,只是暂时还没有找到解决方案。目前我所知道的解决方案有3个:@Query,存储过程和定制实现。
@Query
推荐使用命名参数方式的@Query注解,还是以findByUsername为例:
    @Query("select u from user u where u.username = :username")
    User findByUsername(@Param("username") String username);

存储过程
在User.java增加:
@Entity
@NamedStoredProcedureQuery(name = "User.findByUsername", procedureName = "findByUsername", parameters = {
    @StoredProcedureParameter(mode = ParameterMode.IN, name = "username", type = String.class),
    @StoredProcedureParameter(mode = ParameterMode.OUT, name = "user", type = User.class) })
public class User {
...

在UserRepository中:
    @Procedure("findByUsername")
    User findByUserName(String username);

当然数据库里面需要有一个findByUserName的存储过程。
定制实现
增加一个接口:
package com.healtrav.repository;

import com.healtrav.domain.User;

public interface UserRepositoryCustom {

    public User getByUsername(String username);
}

修改UserRepository的定义:
package com.healtrav.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

import com.healtrav.domain.User;

@RepositoryRestResource(collectionResourceRel = "user", path = "user")
public interface UserRepository
        extends JpaRepository<User, Long>, UserRepositoryCustom {

    User findByUsername(@Param("username") String username);
}

添加UserRepositoryCustom的实现:
package com.healtrav.repository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.webmvc.RepositorySearchesResource;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.ResourceProcessor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.healtrav.domain.User;

@RestController
@RequestMapping("/user")
public class UserRepositoryImpl implements
        UserRepositoryCustom, ResourceProcessor<RepositorySearchesResource> {

    @Autowired
    UserRepository userRepo;

    @Override
    @RequestMapping(value = "/search/getUserByUsername", method = RequestMethod.GET)
    public User getByUsername(
            @RequestParam(value = "username", required = true) String username) {
        return userRepo.findByUsername(username);
    }

    @Override
    public RepositorySearchesResource process(
            RepositorySearchesResource resource) {
        String href = resource.getId().getHref();
        resource.add(new Link(href + "/getByUsername{?username}")
                .withRel("getByUsername"));
        return resource;
    }

}

其实UserRepository继承UserRepositoryCustom没有什么实际的意义,Spring不会自动暴露UserRepositoryImpl到RESTful,意义在于让UserRepository提供了统一的访问接口。UserRepositoryImpl实现了ResourceProcessor反而更加重要,实际上在curl http://localhost:8081/user/search的时候,增加了一个输出:
 "getByUsername" : {
      "href" : "http://localhost:8081/user/search/getByUsernamee{?username}",
      "templated" : true
    }

下一章:用Spring Boot & Cloud,Angular2快速搭建微服务web应用 - AngularJS2客户端
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐