《Spring in Action》第6章-REST services

REST services

编写RESTful风格的controller

SpringMVC 提供了@RequestMapping注解用于映射请求与对应的Controller方法。同时,还提供了以下几个更加详细的注解:

AnnotationHTTP methodTypical use
@GetMappingHTTP GET requestsReading resource data
@PostMapingHTTP POST requestsCreaating a resource
@PutMappingHTTP PUT requestsUpdating a resource
@PatchMappingHTTP PATCH requestsUpdating a resource
@DeleteMappingHTTP DELETE requestsDelete a resource

也可以通过@RequestMapping的method属性来指定HTTP方法。

通过服务获取数据

@RestController
@RequestMapping(path="/design",produces="application/json")
@CrossOrigin(origins="*")
public class DesignTaacoController{
	...

	@GetMapping("recent")
	public Itarable<Taco> recentDesigns(){
		...
		return designs;
 	}
}

上面的Controller与普通的Controller的不同点:

  1. 使用的是@RestController而非Controller

@RestController第一个作用是使被注解的类被Spring容器探测到从而注册为bean;第二个作用告诉Spring被注册的类中的所有请求处理方法的返回值都直接写入响应体,而不是通过Model传递给页面。
我们也可以再类级别使用@Controller注解,在方法级别使用@ResponseBody注解达到同样的效果。

  1. @RequestMapping多了produces参数

这里的produces值为"application/json",这表明该类中的所有方法只响应那些请求头(Header)中Accept属性值为"application/json"的请求;同时也限制了该类的处理方法的返回值全部都为json格式;另外它还允许其它的Controller来处理相同URL请求(只要请求不要求json格式的结果)。

我们也可以指定多个数据返回格式,如:RequestMapping(path="/design",produces={"application/json","text/xml"})。我们只需要加入XML的依赖包就可以实现返回XML格式的数据:

<dependency>
    <groupId>com.fasterxml.jackson.jaxrs</groupId>
    <artifactId>jackson-jaxrs-xml-provider</artifactId>
    <version>2.5.0</version>
</dependency>
  1. 使用了@CrossOrigin注解

作为一个前后端分离的项目,前端页面与后端服务可能不在同一个host和端口上,这种情况下浏览器会阻止前端调用服务。我们可以通过在响应头天骄CORS来消除这一限制。Spring 则通过使用@CrossOrigin注解来实现。这里的origins属性值为 “*” ,表示该服务允许来自所有源的请求。

向服务传递查询参数

通过id查询Taco

@GetMapping("/{id}")
public ResponseEntity<Taco> tacoById(@PathVariable long id){
	Optional<Taco> optTaco = tacoRepository.findById(id);
	if (optTaco.isPresent()){//查询到了结果
		return new ResponseEntity<>(optTaco.get(),HttpStatus.OK);
	}else{
		return new ResponseEntity<>(null,HttpStatus.NOT_FOUND);
	}
}

这个方法与recentDesigns方法的差别:

  1. @GetMapping("/{id}"):
    使用了占位符{},spring会将URL中{id}部分的值作为查询参数,并将值赋给被@PathVarialbe注解的入参id

  2. @PathVariable

该注解有3个属性:

  • name: 指定绑定URL中的那个变量值,默认为"",
  • value: 当无法从URL中找到变量值时,使用该属性值作为默认值。该属性值默认为""。
  • required:如URL中没有找到对应值,将抛出异常。默认为true
  1. 返回ResponseEntity对象

使用改对象,我们可以根据处理结果设置请求结果状态码。如上,查询到了记录,状态码为HttpStatus.OK;没有查到数据,状态码为:HttpStatus.NOT_FOUND更多状态码

向服务发送数据

@PostMapping(consumes="application/json")
@ResponseStatus(HttpStatus.CREATED)
public Taco postTaco(@RequestBody Taco taco){
	return tacoRepository.save(taco);
}
  1. PostMapping(consumes="application/json"):
    指定该方法只处理请求头中,Content-type=application/json的请求。

  2. @ResponseStatus(HttpStatus.CREATED):
    当该方法正常结束时,响应头中的结果状态码为201。改状态码不仅表示请求成功,还表示该请求创建了新的资源。

  3. @RequestBody:
    这个注解非常重要,如果没有这个注解,那么SpringMVC会猜测我们是想要将查询参数或者是表单提交的数据赋值给taco。使用该注解,那么请求体(Request Body) 中的JSON数据将会被绑定给taco。

更新数据

我们有两种方式更新数据,一种是基于HTTP PUT请求,一种是基于HTTP PATCH请求。两种方式的区别在于:

  • HTTP PUT:用于替换整个资源
  • HTTP PATCH:用于更新部分资源属性。

整个两个请求方法只是根据其语义来区分功能的,具体的更新方式还是看我们如何编码实现。下面是简单的实现:

@PutMapping(consumes="application/json")
public Taco update(@ResponseBody Taco taco){
	return tacoRepository.save(taco);
}
@PatchMapping(path="/{id}",consumes="application/json")
public Taco update(@PathVariable long id, @ResponseBody Taco taco){
	Taco oldTaoc = tacoRepository.findById(id);
	if (taco.getName() != null){
		oldTaco.setName(taco.getName());
	}
	...
	...
}

删除数据

@DeleteMapping("/{id}")
@ResponseStatus(code=HttpStatues.NO_CONTENT)
public void deleteOrder(@PathVariable("id") long id){
	tacoRepository.deleteById(id);
}

删除成功后返回的状态码为HttpStatus.NO_CONTENT(204),删除后不需要返回任何数据给客户端,所以返回204给客户端,告诉客户端响应体体中没有响应数据。

使用超媒体(Hypermedia)

Hypermedia(一种应用描述引擎)或 HATEOAS,提供了一种手段,它可以在API返回的资源中创建描述资源资源本身的API(即资源的URL)。

HATEOAS(Hypermedia as the Engine of Application State),也被称为 HAL(Hypertext Application Language),是一种简单的被普遍使用的向JSON中插入超链接的格式。

Spring HATEOAS项目提供了一些了的类和资源整合器,可以在SpringMVC Controller返回资源之前,向资源中添加超链接。使用HATEOAS,就可以避免在客户端硬编码资源路径或者操作资源路径字符串,而是直接使用接口返回的资源路径。

要使用 Spring HATEOAS 就需要加入其依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

添加超链接

Spring HATEOS 提供了两个类:CollectionModleEntityModel,这两个类可以实现向资源中加入超链接。从类名就可以分辨出,CollectionModle用于向集合资源添加超链接;EntityMoel用于向单个的对象资源添加超链接。

@GetMapping("/recent")
public CollectionModel<Taco> recentDesigns(){
	PageRequest page = PageRequest.of(0,12,Sort.by("createAt"));
    List<Taco> tacos = tacoRepository.findAll(page).getContent();
    CollectionModel<EntityModel<Taco>> recentCollection = CollectionModel.wrap(tacos);
	recentCollection.add(new Link("http://localhost:8080/design/recent"),"recents");
	return recentCollection;
}
  1. 返回值不是List<Taco>,而是CollectionModel<Taco>,使用CollectionModel.wrap()将查询结果封装起来。

  2. 调用了CollectionModeladd方法,为List<Taco>资源定义了URL及URL名称

上面的例子我们还是通过硬编码的方式定义的URL,下面我们使用更好的方式来构造URL:

recentCollection.add(WebMvcLinkBuilder.linkTo(DesignController.class).slash("recent").withRef("recents"));
  1. linkTo(DesignController.class):WebMvcLinkBuilder使用DesignController的基本路径作为正在创建的Link的基本路径。
  2. slash("recent"):顾名思义,在构建的Link对象的URL后加上/recent
  3. withRef("recents"):给该URL提供一个相关的名称。

上面的方式还是存在少部分的URL硬编码,使用methodOn我们可以消除所有URL相关的硬编码:

recentCollection.add(WebMvcLinkBuilder.linkTo(methodOn(DesignController.class).recentDesigns()).withRef(recents));

通过上面的方法,WebMvcLinkBuilder使用DesignController的基本路径加上recentDesigns方法的路径组成了URL。

创建资源组装器

前面我们返回的资源中,只有List<Taco>列表的URL,列表类的每个Taco并没有URL,我们需要为每个Taco也加上URL。

  1. 我们需要为Taco类定义一个通用类
@Data
public class TacoModel extends RepresentationModel<TacoModel>{
   private Date createAt;
   private String name;
   private List<Ingredient> ingredients;

}

这个通用类没有Id字段,那是因为我们不需要展示它。在客户端API中,资源自身的URL将会作为资源的唯一标识。

  1. 我们还需要一个资源组装器,用于将Taco列转换为TacoModel:
public class TacoModelAssembler extends RepresentationModelAssemblerSupport <Taco,TacoModel> {
   public TacoModelAssembler() {
       super(DesignTacoController.class,TacoModel.class);
   }

   @Override
   public TacoModel toModel(Taco entity) {
       TacoModel tacoModel = createModelWithId(entity.getId(),entity);
       tacoModel.setCreateAt(entity.getCreateAt());
       tacoModel.setIngredients(entity.getIngredients());
       tacoModel.setName(entity.getName());
       return tacoModel;
   }
}
  • 构造方法:

构造方法调用了父类的构造方法,在上面的例子中,当构造TacoModel实例时,将会使用DesignController来决定Taco的基本URL。

  • createModelWithId()

调用该方法,会返回一个TacoModel实例,且这个实例中已经加入了对应entity的URL。

  1. List<Taco>转换为CollectionModel<TacoModel>:
public CollectionModel<TacoModel> recent(){
    List<Taco> tacos = tacoRepository.findAll();
    CollectionModel<TacoModel> tacoModels = new TacoModelAssemeler().toCollectionModel(tacos);
    tacoModels.add(WebMvcLinkBuilder.linkTo(methodOn(DesignController.class).recent()).withRef("recents"));
}

因为TacoModel中存在List<Ingredient>域,若只是做了以上工作,那么每个Taco对象中的Ingredient域并不包含URL,所以我们还要为Ingredient添加组装器:

  1. Ingredient的通用类和组装器实现跟Taco类似

  2. 需要对Taco的通用类和组装器做一点改变:

  • TacoModel:原本是ingreds域的类型为:List<Ingredient>,现在要改为:CollectionModel<IngredientModel>:

  • TacoModelAssembler:

@Override
public TacoModel toModel(Taco entity) {
	TacoModel tacoModel = createModelWithId(entity.getId(),entity);
	tacoModel.setCreateAt(entity.getCreateAt());
	tacoModel.setIngredients(new IngredentModelAssembler(IngredientController.class,IngredientModel.class).toCollectionModel(entity.getIngredients()));
	tacoModel.setName(entity.getName());
	return tacoModel;
}

给资源命名

在前面的例子中,返回的jsong格式可能是这样的:

{
    "_embedded": {
        "tacoModelList": [
            {
                "createAt": "2020-04-17T14:39:59.000+0000",
                "name": "taco1",
                "ingredients": {
                    "_embedded": {
                        "ingredientModelList": [
                            {
                                "name": "Flour Tortilla",
                                "type": "WRAP",
                                "_links": {
                                    "self": {
                                        "href": "http://localhost:8080/rest/ingredient/FLTO"
                                    }
                                }
                            },
                            {
                                "name": "Lettuce",
                                "type": "VEGGIES",
                                "_links": {
                                    "self": {
                                        "href": "http://localhost:8080/rest/ingredient/LETC"
                                    }
                                }
                            }
                        ]
                    }
                },
                "_links": {
                    "self": {
                        "href": "http://localhost:8080/rest/design/3"
                    }
                }
            },
            {
                "createAt": "2020-04-17T14:42:38.000+0000",
                "name": "taco2",
                "ingredients": {
                    "_embedded": {
                        "ingredientModelList": [
                            {
                                "name": "Flour Tortilla",
                                "type": "WRAP",
                                "_links": {
                                    "self": {
                                        "href": "http://localhost:8080/rest/ingredient/FLTO"
                                    }
                                }
                            },
                            {
                                "name": "Ground Beef",
                                "type": "PROTEIN",
                                "_links": {
                                    "self": {
                                        "href": "http://localhost:8080/rest/ingredient/GRBF"
                                    }
                                }
                            }
                        ]
                    }
                },
                "_links": {
                    "self": {
                        "href": "http://localhost:8080/rest/design/4"
                    }
                }
            }
        ]
    },
    "_links": {
        "recents": {
            "href": "http://localhost:8080/rest/design/recent"
        }
    }
}

CollectionModel<TacoModel>在json中的名称为:tacoModelList,CollectionModel<IngredientModel>在json中的名称为:ingredientModelList。我们可以修改这些名称

@Data
@Relation(value="taco",collectionRelation = "tacos")
public class TacoModel  extends RepresentationModel<TacoModel>{
    private Date createAt;
    private String name;
    private CollectionModel<IngredientModel> ingredients;
}

使用@Relation(value="taco",collectionRelation = "tacos")注释通用类,当返回TacoModel列表时,列表名称为tacos,返回单个TacoModel对象时,名称为taco。

使用以数据为中心的服务

Srping Data REST可以自动为那些基于Spring Data的Repository创建REST API。我们唯一需要做的就是在项目中加入它的依赖:

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

我们定义了一个这样的Repository:

public interface IngredientRepository extends CrudRepository<Ingredient,String>{
}

我们就可以发出一个向http://localhost:8080/ingredients的GET请求,该服务将返回所有的Ingredient,并且还带有资源的超链接:

{
    "_embedded": {
        "ingredients": [
            {
                "name": "Carnitas",
                "type": "PROTEIN",
                "_links": {
                    "self": {
                        "href": "http://localhost:8080/ingredients/CARN"
                    },
                    "ingredient": {
                        "href": "http://localhost:8080/ingredients/CARN"
                    }
                }
            },
            ...
        ]
    },
    "_links": {
        "self": {
            "href": "http://localhost:8080/ingredients"
        },
        "profile": {
            "href": "http://localhost:8080/profile/ingredients"
        }
    }
}

为了让这些接口的URL与普通请求的URL分开,我们可以设置API的基本路径:

spring:
	data:
		rest:
			base-path: /api

这样,获取所有ingredient的URL就变为了:http://localhost:8080/api/ingredinets

与Ingredient相同, 我们也定义了TacoRepository接口,但是访问http://localhost:8080/api/tacos却是404:

调整资源路径与关联名称

Spring Data REST 使用相关实体的 复数形式 来组成URL。但是对于Taco实体,却使用的是错误的复数形式:“tacoes”,而不是"tacos"。所以为了确认所有暴露的API链接,我们可以向API的基础路径:http://localhost:8080/api 发出GET请求:

{
    "_links": {
        "tacoes": {
            "href": "http://localhost:8080/api/tacoes{?page,size,sort}",
            "templated": true
        },
        "users": {
            "href": "http://localhost:8080/api/users"
        },
        "orders": {
            "href": "http://localhost:8080/api/orders"
        },
        "ingredients": {
            "href": "http://localhost:8080/api/ingredients"
        },
        "profile": {
            "href": "http://localhost:8080/api/profile"
        }
    }
}

通过结果我们可以看到,Taco实体的路径和关系名称都是"tacoes"。

除此之外,我们可以在Taco实体类上添加@RestResource注解,人为的指定路径和关系名称:

@Data
@Entity
@RestResource(path = "tacos",rel = "tacos")
public class Taco {
	...
}

这样,我们就可以通过/api/tacos来请求服务了。

分页与排序

前面我们通过/api 的到的结果可以看到,Taco实体的接口路径后面可以带参数"{?page,size,sort}",这些参数就是为分页和排序提供的。

/api/tacoes?page=0&size=12&sort=createAt,desc:按’createAt’字段降序查询 第1页(页数从0开始),每页大小为12。

要使得API支持分页与排序,那么我们的Repository应该继承PagingAndSortingRepository接口,该接口是CrudRepository接口的子接口。

自定义API

至此,我们只有简单的CRUD接口,如果需要复杂功能的接口,还是需要我们在Controller中定义。我们之前使用@RestController注解Controller创建的API任然可以独立于Spring Data API而被访问,但是这样会有两个问题:

  1. 我们自己创建的Controll并没有映射到Spring Data REST的根路径下。
  2. Spring Data REST不会将我们自己创建的Controller作为链接包含在资源中。

我们可以使用@RepositoryRestController来注解自定义Controller,这样,该Controller的路径将会被映射到Spring Data REST的根路径上。如查询最近设计的Taco:

@RepositoryRestController
public class RecentTacosController {
    private TacoRepository tacoRepository;
    @Autowired
    public RecentTacosController(TacoRepository tacoRepository) {
        this.tacoRepository = tacoRepository;
    }
    @GetMapping("/tacos/recent")
    public ResponseEntity<CollectionModel<TacoModel>> recentTacos(){
        PageRequest pageRequest = PageRequest.of(0,2, Sort.by("createAt").descending());
        Iterable<Taco> tacos = tacoRepository.findAll(pageRequest).getContent();
        CollectionModel<TacoModel> collections = new TacoModelAssembler().toCollectionModel(tacos);
        if (tacos.iterator().hasNext()){
            return new ResponseEntity<CollectionModel<TacoModel>>(collections, HttpStatus.OK);
        }
        return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
    }
}

值得注意的是:虽然该注解域@RestController相似,但是它的作用并非像其语义一样是一个RestController,它不会保证将处理方法的返回值自动的写入响应体中。所以我们应该使用@ResponseBody注解处理方法,或者返回ResponseEntity类型的值。

添加自定义链接到Spring Data API中

虽然我们的自定义Controller已经映射到了Spring Data REST的根路径上,但是Spring Data REST API返回的资源中并没有加入这些路径,我们需要声明一个资源处理器bean:

@Bean
public RepresentationModelProcessor<CollectionModel<Taco>> tacoProcessor(EntityLinks links){
    return new RepresentationModelProcessor<CollectionModel<Taco>>() {
        @Override
        public CollectionModel<Taco> process(CollectionModel<Taco> model) {
            model.add(links.linkFor(Taco.class).slash("recent").withRel("recents"));
            return model;
        }
    };
}

通过定义上面的bean,当类型为:CollectionModel<Taco>的资源被组装 之后RepresentationModelAssemblerSupport.toModel或者RepresentationModelAssemblerSupport.toCollection被调用之后),上面的process方法将会被调用。这样当我们请求:/api/tacos时,返回的数据将会添加我们自定义的Controller路径:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring是掠过Java大地的一阵清风。Spring是以反向控制设计原理为基础,无需EJB而功能依然强大的轻量级J2EE开发框架。Spring大大简化了使用接口开发的复杂性,并且加快和简化了应用系统的开发。使用简单JavaBean就可以得到EJB的强大功能。<br>本书介绍了Spring背后的原理,引领你迅速进入对框架的体验之中。结合简短代码片断和贯穿全书的持续示例,本书向你展示了如何创建简单有效的J2EE应用系统。你将看到如何使用先进的开源工具解决持久层问题,以及如何将你的应用系统与其他流行Web框架集成。你将学习如何使用Spring管理大量的基础设施代码,这样你就可以将注意力集中在真正的问题上——重要的业务需要。<br>本书内容:<br>·使用Hibernate、JDO、iBatis、OJB以及JDBC开发持久层;<br>·声明式事务与事务管理;<br>·与其他Web框架集成:Struts、WebWork、Tapestry、Velocity;<br>·访问J2EE服务,如JMS和EJB;<br>·使用AOP解决交叉问题;<br>·企业组应用系统最佳实践。<br>“……一种解释Spring中各个主题的很好途径……我喜欢这本书”<br>——Christian Parker,Adigio公司总裁<br>“……没有其他书籍可以与这本书的实用性相提并论。”<br>——Olivier Jolly,J2EE构架师,Interface SI<br>“我很喜欢这种展示Spring的方式。”<br>——Norman Richards,XDoclet in Action的作者之一<br>“我极力推荐这本书。”<br>——Jack Herrington,Code Generation in Action的作者 <br>----总共8部分rar下载完后解压 -----<br>Spring in Action. 中文版.part1.rar<br>Spring in Action. 中文版.part2.rar<br>Spring in Action. 中文版.part3.rar<br>Spring in Action. 中文版.part4.rar<br>Spring in Action. 中文版.part5.rar<br>Spring in Action. 中文版.part6.rar<br>Spring in Action. 中文版.part7.rar<br>Spring in Action. 中文版.part8.rar<br>

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值