Spring Data REST—两行代码搞定RESTFul(SpringBoot补充)

直接扔出中文官方文档:

https://www.springcloud.cc/spring-data-rest-zhcn.html#getting-started.boot

什么是Spring Data REST

Spring Data REST是基于Spring Data的repository之上,可以把 repository 自动输出为REST资源,目前支持Spring Data JPA、Spring Data MongoDB、Spring Data Neo4j、Spring Data GemFire、Spring Data Cassandra的 repository 自动转换成REST服务。注意是自动。简单点说,Spring Data REST把我们需要编写的大量REST模版接口做了自动化实现。

举个例子,比如你写了如下代码:

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

自定了一个接口UserRepository 继承了JpaRepository,其中泛型中的User是实体类,Long是主键类型,在类的头部加上了一个 @RepositoryRestResource注解,并添加了一个Path为user。

两行代码即可实现User实体类的RESTFul风格的所有接口,比如发送GET请求到127.0.0.1/api/user,返回JSON格式的数据集合(注:”api”为统一前缀),并且每个Item都提供了相应的Detail URI

简单分页查询127.0.0.1:8080/api/user?page=2&size=2 

这里简单的传入了页码也页数,Spring Data REST为我们自动做了分页功能,是不是很炫?还没完,注意下半部分红框圈住的内容,这里Spring Data REST 还为我们返回了上一页,下一页,以及最后一页的URI。然而到目前为止你只写了两行代码,而且Spring Data REST的功能还不止如此,这里只是简单展现下Spring Data REST的魅力而已。

Spring Data REST本身是一个Spring MVC的应用

<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/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>white.yu</groupId>
    <artifactId>spring-data-rest-demo</artifactId>
    <packaging>war</packaging>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-data-rest-demo Maven Webapp</name>
    <url>http://maven.apache.org</url>
    <dependencies>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-rest-webmvc</artifactId>
            <version>2.5.6.RELEASE</version>
        </dependency>
    </dependencies>
    <build>
        <finalName>spring-data-rest-demo</finalName>
    </build>
</project>

可以看到在Maven项目中,我们加入了Spring Data REST,从引入的Jar可以看到其依赖于Spring和Spring MVC,还可以猜测出Spring Data REST提供的REST服务默认返回的JSON格式,并且默认是用的jackSon解析。

Spring MVC配置Spring Data REST

Spring Data REST的配置定义在RepositoryRestMvcConfiguration类中,其中定义了Spring Data REST的默认配置,在Spring MVC中可以采用继承或者使用@import导入的方式导入Spring Data REST的默认配置,如果需要自定义配置,则需要实现RepositoryRestConfigurer接口 或者继承 RepositoryRestConfigurerAdapter然后重写你自己所需要的方法即可

Spring Boot整合Spring Data REST

如果你还没用过请看这篇Spring Boot环境搭建,在Spring Boot项目中,我只需要引入spring-boot-starter-data-rest的依赖,无需任何配置即可使用

   <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.4.3.RELEASE</version>
    </parent>

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

   等等等等等等等…………………………………………………………

具体的看这里:

基础配置 

Spring Data REST的基础配置定义在RepositoryRestConfiguration(org.springframework.data.rest.core.config.RepositoryRestConfiguration)类中。

如果使用的是Spring Boot,则可以在application.properties中直接进行配置。

spring.data.rest.basePath=/api
等等等等……………………………………

还可以通过java文件来配置:

@Configuration
class CustomRestMvcConfiguration {

  @Bean
  public RepositoryRestConfigurer repositoryRestConfigurer() {

    return new RepositoryRestConfigurerAdapter() {

      @Override
      public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        configuration.setBasePath("/api")
      }
    };
  }
}

自定义输出字段

1.隐藏某个字段

public class User {

    /**
     * 指定id为主键,并设置为自增长
     */
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @GenericGenerator(name = "increment", strategy = "increment")
    private long id;
    private String name;
    @JsonIgnore
    private String password;
    private int age;
    private boolean sex;
}

比如在实体对象User中,我们不希望password 序列化未JSON,在上篇博客中说到,Spring Data REST默认使用的是JackSon,则我们就可以使用在需要隐藏的字段添加@JsonIgnore即可 

2、@Projections

@Projection(name="list",types=User.class)
public interface ListUser {
    String getName();
    long getId();
}

也可以通过@Projection注解实现1中的效果, 
请求URL为:127.0.0.1:8080/user?projection=list 
返回数据:

{
  "_embedded": {
    "users": [
      {
        "name": "小白鱼",
        "id": 1,
        "_links": {
          "self": {
            "href": "http://127.0.0.1:8080/user/1"
          },
          "user": {
            "href": "http://127.0.0.1:8080/user/1{?projection}",
            "templated": true
          }
        }
      },
      {
        "name": "小白",
        "id": 2,
        "_links": {
          "self": {
            "href": "http://127.0.0.1:8080/user/2"
          },
          "user": {
            "href": "http://127.0.0.1:8080/user/2{?projection}",
            "templated": true
          }
        }
      },
      {
        "name": "小 鱼 ",
        "id": 3,
        "_links": {
          "self": {
            "href": "http://127.0.0.1:8080/user/3"
          },
          "user": {
            "href": "http://127.0.0.1:8080/user/3{?projection}",
            "templated": true
          }
        }
      },
      {
        "name": "white yu",
        "id": 4,
        "_links": {
          "self": {
            "href": "http://127.0.0.1:8080/user/4"
          },
          "user": {
            "href": "http://127.0.0.1:8080/user/4{?projection}",
            "templated": true
          }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://127.0.0.1:8080/user"
    },
    "profile": {
      "href": "http://127.0.0.1:8080/profile/user"
    }
  },
  "page": {
    "size": 20,
    "totalElements": 4,
    "totalPages": 1,
    "number": 0
  }
}

@Projection还可以用来建立虚拟列

@Projection(name="virtual",types=User.class)
public interface VirtualUser {

    @Value("#{target.name} #{target.age}") 
    String getFullInfo();

}

这里把User中的name和age合并成一列,这里需要注意String getFullInfo();方法名前面一定要加get,不然无法序列化为JSON数据 
url:http://127.0.0.1:8080/user?projection=virtual
返回数据:

{
  "_embedded": {
    "users": [
      {
        "fullUser": "小白鱼 25",
        "_links": {
          "self": {
            "href": "http://127.0.0.1:8080/user/1"
          },
          "user": {
            "href": "http://127.0.0.1:8080/user/1{?projection}",
            "templated": true
          }
        }
      },
      {
        "fullUser": "小白鱼 25",
        "_links": {
          "self": {
            "href": "http://127.0.0.1:8080/user/2"
          },
          "user": {
            "href": "http://127.0.0.1:8080/user/2{?projection}",
            "templated": true
          }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://127.0.0.1:8080/user"
    },
    "profile": {
      "href": "http://127.0.0.1:8080/profile/user"
    }
  },
  "page": {
    "size": 20,
    "totalElements": 2,
    "totalPages": 1,
    "number": 0
  }
}

@Projection定义的数据格式还可以直接配置到Repository之上,就像下面代码中的这样

@RepositoryRestResource(path="user",excerptProjection=ListUser.class)
public interface UserRepository extends JpaRepository<User, Long>{

}

配置之后返回的JSON数据会按照ListUser定义的数据格式进行输出

三、屏蔽自动化方法

在实际生产环境中,不会轻易的删除用户数据此时我们不希望DELETE的提交方式生效,可以添加@RestResource注解,并设置exported=false即可屏蔽Spring Data REST的自动化方法
比如我们不想轻易的暴露按主键删除的方法,只需要写如下代码

@RepositoryRestResource(path="user",excerptProjection=ListUser.class)
public interface UserRepository extends JpaRepository<User, Long>{

    @RestResource(exported = false)
    @Override
    public void delete(Long id);
}

自定义查询方法 

通常会有这样的需求,根据给定的字段查找相应表中的数据对象。比如在前几篇博客中定义的User实体来,需要一个按照name值查到与之对应的数据对象返回,只需要在UserRopository中定义如下代码:

 /**
     * 根据用户名称查找用户
     */
    @RestResource(path="name",rel="name")
    public User findByName(@Param("name") String name);

一行非常简单的代码的代码,满足了我们的需求。我们并没有做任何实现,只是声明了一个findByName的方法而已,方法签名已经告诉Spring Data Jpa足够的信息来创建这个方法的实现了。
请求URL:http://127.0.0.1:8080/user/search/name?name=小白鱼
返回数据:

{
  "name" : "小白鱼",
  "age" : 25,
  "sex" : false,
  "_links" : {
    "self" : {
      "href" : "http://127.0.0.1:8080/user/1"
    },
    "user" : {
      "href" : "http://127.0.0.1:8080/user/1{?projection}",
      "templated" : true
    }
  }
}

当创建Repository实现的时候,Spring Data会检查Repository接口的所有方法,解析方法的名称,并基于被持久化的对象来试图推测方法的目的。本质上,Spring Data定义了一组小型的领域特定语言(domain-specific language ,DSL),在这里,持久化的细节都是通过Repository方法的签名来描述的。
Spring Data能够知道这个方法是要查找User的,因为我们使用User对JpaRepository进行了参数化。方法名findByName确定该方法需要根据name属性相匹配来查找User,而name是作为参数传递到方法中来的。

findByName()方法非常简单,但是Spring Data也能处理更加有意思的方法名称。Repository方法是由一个动词、一个可选的主题(Subject)、关键词By以及一个断言所组成。在findByName()这个样例中,动词是find,断言是name,主题并没有指定,暗含的主题是User。


Spring Data允许在方法名中使用四种动词:get、read、find和count。其中,动词get、read和find是同义的,这三个动词对应的Repository方法都会查询数据并返回对象。而动词count则会返回匹配对象的数量,而不是对象本身。
在断言中,会有一个或多个限制结果的条件。每个条件必须引用一个属性,并且还可以指定一种比较操作。如果省略比较操作符的话,那么这暗指是一种相等比较操作。不过,我们也可以选择其他的比较操作,包括如下的种类:

IsAfter、After、IsGreaterThan、GreaterThan 
IsGreaterThanEqual、GreaterThanEqual 
IsBefore、Before、IsLessThan、LessThan 
IsLessThanEqual、LessThanEqual 
IsBetween、Between 
IsNull、Null 
IsNotNull、NotNull 
IsIn、In 
IsNotIn、NotIn 
IsStartingWith、StartingWith、StartsWith 
IsEndingWith、EndingWith、EndsWith 
IsContaining、Containing、Contains 
IsLike、Like 
IsNotLike、NotLike 
IsTrue、True 
IsFalse、False 
Is、Equals 
IsNot、Not 
要对比的属性值就是方法的参数。

模糊查询

@RestResource(path="nameStartsWith",rel="nameStartsWith")
    public List<User> findByNameStartsWith(@Param("name") String name);

需求查询以name为white开始的用户,则 
查询URL为:http://127.0.0.1:8080/user/search/nameStartsWith?name=white

忽略大小写查询

要处理String类型的属性时,如果需要忽略大小写则可以在方法签名中加入IgnoringCase,这样在 
执行对比的时候就会不再考虑字符是大写还是小写。例如,要在name属性上忽略大小写,那么可以将方法签名改成如下的形式:

@RestResource(path="nameStartsWith",rel="nameStartsWith")
    public List<User> findByNameStartsWithIgnoringCase(@Param("name") String name);

多条件查询

如果需要匹配多个添加则用And和Or连接,比如:

 @RestResource(path="nameAndAge",rel="nameAndAge")
    public List<User> findByNameAndAge(@Param("name")String name ,@Param("age")int age);

排序

可以在方法名称的结尾处添加OrderBy,实现结果集排序。比如可以按照User的Age降序排列

  @RestResource(path="nameStartsWith",rel="nameStartsWith")
    public List<User> findByNameStartsWithOrderByAgeDesc(@Param("name") String name);

这里只是初步体验了所能声明的方法种类,Spring Data JPA会为我们实现这些方法。现在,我们只需知道通过使用属性名和关键字构建Repository方法签名,就能让Spring Data JPA生成方法实现,完成几乎所有能够想象到的查询。不过,Spring Data这个小型的DSL依旧有其局限性,有时候通过方法名称表达预期的查询很烦琐,甚至无法实现。如果遇到这种情形的话,Spring Data能够让我们通过@Query注解来解决问题。