65.9K
CodeProject 正在变化。 阅读更多。
Home

OpenRest

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2015 年 9 月 12 日

Apache

5分钟阅读

viewsIcon

13365

Spring Data Rest 扩展。

引言

也许阅读本文的各位都熟悉 Spring Data Rest 框架,它简化了 REST API 的构建。要列出它所有的特性和优点,我需要花费大量的文字,但这些内容已经在互联网上的许多博客上写过了。因此,我将重点关注它的两个主要局限性。

过滤资源

Spring Data Rest 的一个特性是将其查询方法导出为 RESTful 端点。这对于简单的情况来说非常棒,例如,只需编写一行代码即可为您的 API 提供一个根据用户名过滤用户的端点。不幸的是,这些查询方法是不可分割的,无法相互组合。这意味着,解决一些复杂情况(如带可选参数的查询)的开发人员必须编写多个查询方法,或者编写一个自定义方法并使用控制器将其导出。

创建和更新资源

应用程序中的模型和视图应该分离。当涉及到 `GET` 请求时,Spring Data Rest 非常好地处理了这个问题。它提供了 Projections 机制,完美地将实体与视图分离开来。不幸的是,对于创建和更新资源,没有类似的特性。

OpenRest

为了填补上两段引言中描述的空白,我创建了一个名为 OpenRest 的 Spring Data Rest 扩展,它主要有两个功能:导出谓词(predicates)而不是完整的查询,客户可以在请求时将它们组合起来。OpenRest 的第二个功能是为 `POST`、`PUT` 和 `PATCH` 请求提供数据传输对象(Data Transfer Objects)。由于 Spring Data Rest 是一个很棒的代码库,我在编写 OpenRest 时的一个主要原则是尽可能少地修改它,并允许用户在需要时关闭它并使用 Spring Data Rest 的基本功能。

使用示例:

展示 OpenRest 特性的最佳方式是使用一个示例应用程序。我将只解释库中最重要 parts。其他所有内容您都可以在 OpenRest 文档中找到:https://github.com/konik32/openrest。让我们构建一个简单的应用程序来管理一家示例公司及其部门的客户。

配置

要启用 OpenRest 功能,您必须在您的主配置类上添加 `@EnableOpenRest` 注解。

@SpringBootApplication
@EnableOpenRest
public class Application {

       public static void main(String[] args) throws Exception {
             SpringApplication.run(Application.class, args);
       }
}

模型

@Embeddable
public class Address {

       private String city;
       private String street;
       private String zip;
       private String homeNr;

}

@Embeddable
public class CompanyData {

       private String nip;
       private String regon;
       private String krs;

}

@Table(name = "contactPersons")
@Entity
public class ContactPerson extends AbstractPersistable<Long> {

       private String name;
       private String surname;
       private String email;
       private String phoneNr;

}

@Table(name = "clients")
@Entity
public class Client extends AbstractPersistable<Long> {

       private String name;
       private String phoneNr;
       @Embedded
       private Address address;
       @Embedded
       private CompanyData companyData;
       @ManyToOne
       private Department department;
       @ManyToMany
       @JoinTable(...)
       private Set<Product>products;
       public void addProduct(Product product) {
             ...
       }
}


@Table(name = "departments")
@Entity
public class Department extends AbstractPersistable<Long> {

       private String name;
       @Embedded
       private Address address;
       @OneToMany
       private List<ContactPerson>contactPersons;
       private Boolean active;
       public void addContactPerson(ContactPersoncontactPerson) {...}

}

存储库

要将实体导出为资源,我们需要创建简单的 Spring Data Rest 存储库接口,并继承 `PredicateContextQueryDslRepository<Entity>`。

数据传输对象

在 OpenRest 中,创建和更新资源是通过 `DTO` 来完成的。我们声明带有字段的类,这些字段将从 `POST`、`PUT`、`PATCH` 请求的内容中设置。实体对象将通过映射同名字段(`POST`、`PUT` 请求)或通过 getter/setter 对(`PATCH` 请求)从 `DTO` 创建/合并(其他字段将被忽略)。当然,`DTO` 也可以嵌套。

@Data
@Dto(entityType = Client.class, name = "clientDto", type = DtoType.CREATE)
public class ClientDto {

       private String name;
       @Valid
       private AddressDto address;
       @Valid
       @ValidateExpression("#{@validators.validateCompanyDataDto(dto.companyData)}")
       private CompanyDataDto companyData;
       private Department department;

}

如果 `DTO` 字段与实体字段的自动映射不足,并且您需要对该过程进行更多手动控制,您可以声明自定义创建者/合并器。您所要做的就是实现 `EntityFromDtoCreator<Entity,DTO>` 接口,并将它的类型传递给 `@Dto` 注解。例如

@Data
@Dto(entityType = Address.class, name = "addressDto", type = DtoType.BOTH, entityCreatorType=AddressDtoCreator.class)
public class AddressDto {

       @Pattern(regexp="^(.*)[ ]+(.*), ([0-9]{2}-[0-9]{3})[ ]+(.*)$")
       private String address;
}


@Component
public class AddressDtoCreator implements EntityFromDtoCreator<Address, AddressDto> {

       private static final Pattern ADDRESS_PATTERN = Pattern.compile("^(.*)[ ]+(.*), ([0-9]{2}-[0-9]{3})[ ]+(.*)$");

       @Override
       public Address create(AddressDto from, DtoInformation dtoInfo) {
             Address address = new Address();
             Matcher matcher = ADDRESS_PATTERN.matcher(from.getAddress().trim());
             if (matcher.find()) {
                    address.setStreet(matcher.group(1));
                    address.setHomeNr(matcher.group(2));
                    address.setZip(matcher.group(3));
                    address.setCity(matcher.group(4));
                    return address;
             }
             return null;
       }
}

在完成这四步并实现 `DTO` 的其余部分后,我们可以使用以下 `JSON` 请求创建客户资源:

POST /clients?dto=clientDto

{
   "name": "client 1",
   "address": {
       "address": "Krakowska 57, 33-300 Warszawa"
   },
   "companyData": {
       "nip": "23232323",
       "regon": "213123",
       "krs": "123123"
   },
   "department": "/departments/1"
}

OpenRest 支持一个实体有多个 `DTO`,所以我们必须传递 dto 查询参数,其中包含我们要使用的 `DTO` 的名称。当 `Dto` 参数缺失时,OpenRest 会抛出异常,因此它是必需的。

现在,让我们看看 `ContactPerson` 类。它是一个可以与其他许多实体关联的实体。在这种情况下,关联将是单向的,就像 `Department` 实体一样。如果我们想创建一个 `ContactPerson` 资源并将其连接到它的关联,我们将不得不发送两个请求或创建一个自定义控制器。在 OpenRest 中,我们可以利用 `DTO` 和事件处理器来实现上述目标。

@Dto(entityType = ContactPerson.class, name = "contactPersonDto", type = DtoType.BOTH)
@Data
public class ContactPersonDto {

       private String name;
       private String surname;
       private String email;
       private String phoneNr;
}


@Getter
@Setter
@Dto(entityType = ContactPerson.class, name = "departmentContactPersonDto", type = DtoType.CREATE)
public class DepartmentContactPersonDto extends ContactPersonDto {

       @NotNull
       private Department department;
}


@RepositoryEventHandler(ContactPerson.class)
@Component
public class ContactPersonEventHandler {

       @Autowired
       private DepartmentRepositorydepartmentRepository;

       @HandleAfterCreateWithDto(dto = DepartmentContactPersonDto.class)
       public void addContactPersonToCounty(ContactPerson cp, DepartmentContactPersonDto dto) {
             dto.getDepartment().addContactPerson(cp);
             departmentRepository.save(dto.getDepartment());
       }

}

POST /contactPersons?dto=departmentContactPersonDto

{
    "name": "Jan",
    "surname": "Kowalski",
    "email": "jan.kowalski@example.com",
    "department": "/departments/1"
}

要查看如何使用 `DTO` 更新资源,我们可以分析更改用户密码的示例。

@Dto(entityType=User.class, type=DtoType.MERGE, name="updatePasswordDto")
@Data
public class UpdatePasswordDto {

       private String password;
       @ValidateExpression("#{@validators.validatePassword(dto.oldPassword)}")
       private String oldPassword;
       @ValidateExpression("#{dto.confirmPassword.equals(dto.password)}")
       private String confirmPassword;

}

PATCH /users/1?dto=updatePasswordDto

{
    "password": "newPassword",
    "oldPassword": "password",
    "confirmPassword": "newPassword"
}

乍一看,OpenRest `DTO` 机制可能似乎违反了 DRY 原则(Don't Repeat Yourself)。对于简单的情况,它确实如此,但对于复杂的情况(例如,当您需要额外的字段来计算、验证实体的字段时),将视图与模型分离有许多优点,有时是不可避免的。

过滤资源

在创建了一些资源后,是时候显示过滤后的列表了。我们之前声明了一个客户存储库,由于 `PredicateContextQueryDslRepository<Entity>` 总是与 `ExpressionRepository` 成对出现,我们也必须声明它。

@RepositoryRestResource(path = "clients")
public interface ClientRepository extends PagingAndSortingRepository<Client, Long>,PredicateContextQueryDslRepository<Client> {}


@ExpressionRepository(Client.class)
public class ClientExpressionRepository{}

现在我们可以通过 `GET` 请求显示所有客户的分页列表:

GET /clients?orest

如果请求是为了由 OpenRest 处理,它需要 `orest` 查询参数。事实上,没有此参数的请求将不会被接受。上面的请求返回所有客户的分页列表。让我们添加一个搜索方法,用于查找由我们公司某个部门支持的客户。

@ExpressionRepository(Client.class)
public class ClientExpressionRepository{

     @ExpressionMethod(searchMethod = true)
     public BooleanExpression departmentIdEq(Long departmentId) {
           return QClient.client.department.id.eq(departmentId);
     }
}

以及 `GET` 请求:

GET /clients/search/departmentIdEq(1)?orest

在我们的例子中,一个部门负责整个国家的客户。如果我们能添加一些过滤器来查找位于克拉科夫(Cracow)且名字以“Media”开头的客户,那就太好了。我们只需要编写两个表达式方法:

@ExpressionMethod
public BooleanExpression cityEq(String city){
     return QClient.client.address.city.eq(city);
}

@ExpressionMethod
public BooleanExpression nameStartsWith(String name){
     return QClient.client.name.startsWith(name);
}

并通过使用逻辑运算符 `;and;` 和 `;or;` 连接预定义的谓词名称来执行 `GET` 请求:

GET /clients/search/departmentIdEq(1)?orest&filters=cityEq(Cracow);and;nameLike(Media)

其他示例

GET /clients?orest&filters=cityEq(Cracow);or;nameLike(Media)

GET /clients?orest&filters=departmentIdEq(Cracow);or;nameLike(Media);and;cityEq(Cracow)

我们示例公司有一些封闭的部门。要从每个请求中过滤掉它们,我们可以创建一个静态过滤器。

@StaticFilter
@ExpressionMethod
public BooleanExpression active() {
     return QDepartment.department.active.eq(true);
}

由于 `ExpressionRepositories` 是 bean,因此对某些端点(例如 `/clients/search/departmentIdEq(1)`)进行授权很容易。这可以通过添加到 `ExpressionMethod` 的 `@PreAuthorize` 注解来完成。

结论

在本文中,我向您展示了一些使用 OpenRest 核心功能的简单示例,但还有更多。如果您对此感兴趣,请访问 https://github.com/konik32/openrest,阅读文档,克隆 示例,进行实验,并给我一些反馈。

© . All rights reserved.