抛弃Servlet API和Postman开发RESTful

共 9061字,需浏览 19分钟

 ·

2020-10-16 21:07

导读

Spring WebFlux由Spring 5.0框架首次引入。它具有无需Servlet、异步两大特征,从而更好地提高Web应用的可伸缩性。

Spring WebFlux简介





Spring WebFlux由Spring 5.0框架首次引入。与传统Spring MVC相比,主要提供了如下两个优势:


  • 完全脱离了Servlet API。使用Spring WebFlux开发Web应用时,Servlet容器都成了可选项,默认使用Reactor Netty作为服务器。


  • Spring WebFlux实现了完全的异步非阻塞,可以很好地支持反应式流(Reactive Stream)编程范式,也能支持背压(back pressure)等特征。


Reactor框架采用Mono和Flux两个类代表消息发布者,因此它们都实现了CorePublisher接口,它们的区别在于:


  • Mono代表0~1个非阻塞数据;而Flux则代表0~个非阻塞序列。


  • Mono相当于只是一个Optional值;而Flux才是Stream。


简单来说,Mono包含多个数据项,而Flux能包含多个数据项。Spring WebFlux一样也要用Mono和Flux这两个类。


Spring WebFlux就是基于Reactor实现的,其中Flux名称就是来自Reactor中的Flux类,WebFlux包括了对反应式HTTP、服务器推送事件(SSE:Server Send Event)及WebSocket的支持。


Spring WebFlux提供了两种开发方式:


  • 使用类似Spring MVC的注解方式。在这种方式下,依然使用@Controller、@RequestMapping等注解修饰类、方法即可。


  • 使用函数式编程模型的方式。在这种方式下,程序使用RouterFunction来注册映射地址和处理器方法之间路由关系。


上面这两种编程模型只是形式上有所不同(代码编写方式上存在不同),它们本质上完全是一样的,它们都运行在相同的反应式流的基础之上。


使用注解开发WebFlux





下面先使用@Controller、@RequestMapping等注解来开发Spring WebFlux应用。依然按惯例创建一个基于maven-archetype-quickstart的Maven项目,并让其pom.xml文件继承spring-boot-starter-parent,并添加spring-boot-starter-webflux.jar的依赖。


接下来定义如下控制器类:

程序清单:Annotation\src\main\java\org\crazyit\app\controller\ItemController.java@RestController@RequestMapping("/item")public class ItemController{    @GetMapping("/hello")    public Mono hello()    {        return Mono.just("Hello WebFlux");    }}


查看该类代码,不难发现该控制器类与Spring MVC应用的控制器类非常相似,它们同样使用@Controller或@RestController注解来修饰控制器类、同样使用@RequestMapping或其变体注解修饰处理方法;区别只是处理方法的返回值,WebFlux应用的控制器的返回值类型是Mono或Flux(此处是Mono)。


Mono和Flux正是Reactor框架中消息发布者API,它们都实现了CorePublisher接口,这就表示采用了基于“订阅-发布”的异步模式。


本应用的主类并没有任何改变,依然通过SpringApplication的静态run()方法来运行由@SpringBootApplication注解修饰的类即可。


运行该应用的主类来启动应用,将会在控制台看到如下输出:


Netty started on port(s): 8080


从上面输出可以看出,WebFlux应用默认使用Netty作为嵌入式服务器,不再使用Tomcat作为服务器。


然后使用浏览器或Postman向http://localhost:8080/item/hello发送GET请求,即可看到服务器生成如下响应:


Hello WebFlux


上面处理方法只是返回的Mono对象只是包含一个简单的String数据,下面定义的处理方法返回的Mono对象将会包含复合对象。在ItemController类中添加如下方法:


程序清单:Annotation\src\main\java\org\crazyit\app\controller\ItemController.java@Autowiredprivate ItemService itemService;@GetMapping("/{id}")public Mono getByItemId(@PathVariable("id") Integer id){    return Mono.justOrEmpty(this.itemService.getItemById(id))            .switchIfEmpty(Mono.error(new ItemNotFoundException("商品找不到")));}@PostMapping("")public Mono create(@RequestBody Item item){    return Mono.just(this.itemService.createOrUpdate(item));}@PutMapping("")public Mono update(@RequestBody Item item){    Objects.requireNonNull(item);    return Mono.just(this.itemService.createOrUpdate(item));}@DeleteMapping("/{id}")public Mono delete(@PathVariable("id") Integer id){    return Mono.justOrEmpty(this.itemService.delete(id));}


上面这些处理方法同样很简单,它们调用itemService组件来执行CRUD操作,由于itemService的这4个CRUD方法的返回值只是单个Item对象或null,因此程序只要将该返回值放入Mono对象,这样这些处理方法的返回值就变成了消息发布者。


上面控制器类所依赖的ItemService组件实现类代码如下:

程序清单:Annotation\src\main\java\org\crazyit\app\service\impl\ItemService.java@Servicepublic class ItemServiceImpl implements ItemService{    private final Map data = new ConcurrentHashMap<>();    private static final AtomicInteger idGenerator = new AtomicInteger(0);    @Override    public Collection list()    {        return this.data.values();    }    @Override    public Item getItemById(Integer id)    {        return this.data.get(id);    }    @Override    public Item createOrUpdate(Item item)    {        // 修改用户        if (item.getId() != null && data.containsKey(item.getId()))        {            this.data.put(item.getId(), item);        }        else        {            Integer id = idGenerator.incrementAndGet();            item.setId(id);            this.data.put(id, item);        }        return item;    }    @Override    public Item delete(Integer id)    {        return this.data.remove(id);    }}

正如上面代码所看到的,本Service组件并未依赖DAO组件来访问真正的数据库,而是使用内存中Map来模拟内存数据库:当程序需要添加记录时就向Map中添加一个key-value对;当程序需要删除记录时就删除一个key-value对。


提示

使用Map模拟内存中的数据库在学习控制器层和Service层开发时很有用,因为这样可以避免涉及数据库开发,从而更好地聚焦正在学习的内容。

运行该应用的主类来启动应用,然后可使用Postman来发送GET、POST、PUT、DELETE请求来测试上面这些处理方法。


使用curl代替Postman





本节打算教读者使用curl来测试它们。


提示

curl是一个Linux和windows系统都支持的命令行工具,如果能熟练地使用curl工具,你会发发现它非常强大,而且用起来非常方便——唯一的缺点是要记几条命令。读者可登录https://curl.haxx.se下载和安装curl工具,并可参考https://curl.haxx.se/docs/manpage.html快速掌握该工具的用法。当你熟练掌握它之后,你会发现它比Postman更高效、更好用。


curl工具的基本用法如下:


curl 选项 URL


启动命令行工具,执行如下命令:


curl -H "Content-Type: application/json" -X POST -d @item.json http://localhost:8080/item

上面命令涉及如下几个选项:


  • -H:该选项用于指定请求头。

  • -X:该选项用于指定请求方法,可指定为GET、POST、PUT、DELETE等。

  • -d:该选项用于指定请求数据。请求数据即可直接给出,也可通过读取文件,带@符号就表示读取文件内容来作为请求数据。


提示

读者可能会把某个字符之间的间距当成空格。在这里可以告诉大家关于计算机命令格式的一个常识:空格是命令格式中非常敏感的字符。基本常识是:每个选项名(如-H、-X、-d等)与选项值之间有空格;选项值整体不能有空格,否则计算机会尝试将它空格后面的内容解释成下一个选项,因此如果选项值之间有空格或特殊字符,需要用双引号括起来,比如上面"Content-Type: application/json"就是-H选项的选项值,它需要用引号括起来;第二个选项名与前一个选择值之间有空格,例如-X选项与前面的"Content-Type: application/json"之间有空格,-d选项与前面的POST之间有空格。


如果在Windows平台上使用curl命令,最好使用读取文件的方式来提交请求数据——因为Windows平台的命令行窗口默认采用GBK字符集,因此处理起来比较烦人。


上面命令中指定了-d @item.json选项,这意味着curl命令要读取当前目录下的item.json文件内容作为请求数据。因此还需在当前目录(当你在Windows命令行窗口中执行curl命令时,命令行窗口中>符号前的字符串就是当前目录)下使用UTF-8字符集创建如下item.json文件。


{    "name": "疯狂Java讲义",    "price": 128}


执行上面命令,将会在命令行窗口看到如下输出:


curl -H "Content-Type: application/json" -X POST -d @item.json http://localhost:8080/item{"id":1,"name":"疯狂Java讲义","price":128.0}


上面第二行输出就是服务器响应,这就表明向服务器发送POST请求添加数据成功。


将item.json的数据略作修改(只能修改name属性或price属性的值),再次发送上面POST请求即可向服务器添加新的Item。


执行如下命令来发送GET请求:


curl http://localhost:8080/item/1


上面命令没有指定任何选项,这意味着发送默认的GET请求,没有请求数据,没有指定额外的请求头。执行上面命令将会看到如下输出:


curl http://localhost:8080/item/1{"id":1,"name":"疯狂Java讲义","price":128.0}


在当前目录下使用UTF-8字符集创建如下item_update.json文件。


{    "id": 1,    "name": "疯狂Android讲义",    "price": 128}


上面JSON字符串定义的Item对象指定了id属性,该字符串可用于更新id为1的Item对象。然后执行如下命令来发送PUT请求:


curl -H "Content-Type: application/json" -X PUT -d @item_update.json http://localhost:8080/item


上面命令与前面的执行POST请求的命令基本相同,只是将-X选项改成了PUT,并改为读取当前目录下item_update.json文件的内容作为请求数据。

执行上面命令将会看到如下输出:


curl -H "Content-Type: application/json" -X PUT -d @item_update.json http://localhost:8080/item{"id":1,"name":"疯狂Android讲义","price":128.0}

这样就服务端id为1的Item进行了修改,再次执行curl http://localhost:8080/item/1命令来查看id为1的Item对象,即可看到它的name属性值是修改后的属性值了。


执行如下命令来发送DELETE请求:


curl -X DELETE http://localhost:8080/item/1


上面命令使用-X选项指定了发送DELETE请求,执行上面命令将会看到如下输出:


curl -X DELETE http://localhost:8080/item/1{"id":1,"name":"疯狂Android讲义","price":128.0}


上面命令执行完成后,服务端id为1的Item对象就被删除了。如果再次执行curl http://localhost:8080/item/1命令来查看id为1的Item对象,即可看到如下输出:


curl http://localhost:8080/item/1{"timestamp":"2020-10-14T23:37:31.472+00:00","path":"/item/1","status":500,"error":"Internal Server Error","message":"商品找不到",...


从服务器响应即可看出,id为1的Item对象不再存在。


上面4个处理方法返回的都是包含单个数据的Mono对象,当服务器相应是多项数据时,可使用Flux返回值来定义发布者。在ItemController中添加如下处理方法:


程序清单:Annotation\src\main\java\org\crazyit\app\controller\ItemController.java@GetMapping("")public Flux list(Integer size){    if (size == null || size == 0)    {        size = 5;    }    return Flux.fromIterable(this.itemService.list()).take(size);}


上面代码调用Flux的fromIterable()方法来将整个序列包含的数据变成消息发布者,然后调用Flux的take()方法来取出指定数量的数据项——本例将会根据size请求参数(如果该参数不存在,则使用默认值5)来取出数据项。


再次运行主程序来启动应用,先使用curl发送POST请求添加几条数据,,然后使用curl执行如下命令:


curl http://localhost:8080/item?size=3


上面命令没有指定任何选项,这意味着它依然是发送GET请求,但发送请求时指定了size参数,运行该命令将会看到如下输出:


curl http://localhost:8080/item?size=3[{"id":1,"name":"疯狂Java讲义","price":128.0},{"id":2,"name":"疯狂Python讲义","price":118.0},{"id":3,"name":"疯狂Android讲义","price":138.0}]


到此为止,可能有读者会对WebFlux感到有点失望,好像WebFlux与Spring MVC并没有什么区别,不仅开发方式差不多,连服务器生成的响应也差不多——实际上前面已经说过,WebFlux的变化主要是两点:①、彻底抛弃Servlet API;②、基于订阅-发布的异步机制。而这两点的区别主要体现在底层服务器能以较小的线程池处理更高的并发,从而提高应用的可伸缩性,它的区别往往并不体现在表面上。


当然异步响应也还是略有不同的,在ItemController中再次添加如下处理方法:

程序清单:Annotation\src\main\java\org\crazyit\app\controller\ItemController.java@GetMapping(value = "", produces = "application/stream+json")public Flux list(){    // 需要周期生成数据,使用 Flux.interval    return Flux.interval(Duration.ofMillis(2000))            .onBackpressureDrop()            // 每隔interval,执行一次itemService.list()的方法            .map((interval) -> itemService.list())            // 将List转换成Flux            .flatMapIterable(item -> item)            .log("生成信息");}


上面@GetMapping注解中指定了produces = "application/stream+json"),这意味着该处理方法将负责处理Accept请求头为“application/stream+json”的GET请求。


上面list()方法中使用了Flux的interval()方法来周期性地生成数据,而且由于客户端可接受“流式”JSON响应,这样该方法将可每隔2秒向客户端发送一次响应。


再次运行主程序来启动应用,先使用curl发送POST请求添加2条数据,,然后使用curl执行如下命令:


curl http://localhost:8080/item -i -H "Accept: application/stream+json"


上面命令使用-H选项指定了Accept请求头,还使用了一个 -i选项,该选项无需选项值,它的作用是控制输出服务器响应的响应头。


运行上面命令将可看到如下输出:


curl http://localhost:8080/item -i -H "Accept: application/stream+jsonHTTP/1.1 200 OKtransfer-encoding: chunkedContent-Type: application/stream+json

{"id":1,"name":"疯狂Python讲义","price":118.0}{"id":2,"name":"疯狂Java讲义","price":128.0}{"id":1,"name":"疯狂Python讲义","price":118.0}{"id":2,"name":"疯狂Java讲义","price":128.0}...


此时将会看到服务器响应不断地“跳出”,每次生成两项数据——这是因为Flux订阅者每次获取的都只有两条数据(itemService.list()方法只返回两条数据)。


启动另一个命令行窗口,再次使用curl执行POST请求添加一个Item对象,再次切换回原来的命令行窗口,此时由于系统中包含了3个Item对象(itemService.list()方法返三条数据),此时将可看到服务器每次会生成三条数据的响应。


关于更多Spring编程的深入技巧可参考李刚老师的《轻量级Java Web企业应用实战》

喜欢请分享到朋友圈

长按二维码轻松关注


浏览 12
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报