微服务专题03-REST

目录导航

    • 前言
    • REST理论基础
      • RPC
      • Cacheability(可缓存性)
      • HTTP 状态码
      • Uniform interface(统一接口)
        • URI & URL
        • 资源操作
        • 自定义注解
      • 自描述消息
        • 注解驱动
        • 接口编程
        • 媒体类型(`MediaType`)
      • `@EnableWebMvc` 源码导读
    • 后记

前言

前面的章节我们讲了Spring Web MVC 。本节,继续微服务专题的内容分享,共计16小节,分别是:

  • 微服务专题01-Spring Application
  • 微服务专题02-Spring Web MVC 视图技术
  • 微服务专题03-REST
  • 微服务专题04-Spring WebFlux 原理
  • 微服务专题05-Spring WebFlux 运用
  • 微服务专题06-云原生应用(Cloud Native Applications)
  • 微服务专题07-Spring Cloud 配置管理
  • 微服务专题08-Spring Cloud 服务发现
  • 微服务专题09-Spring Cloud 负载均衡
  • 微服务专题10-Spring Cloud 服务熔断
  • 微服务专题11-Spring Cloud 服务调用
  • 微服务专题12-Spring Cloud Gateway
  • 微服务专题13-Spring Cloud Stream (上)
  • 微服务专题14-Spring Cloud Bus
  • 微服务专题15-Spring Cloud Stream 实现
  • 微服务专题16-Spring Cloud 整体回顾

本节内容重点为:

  • REST 理论基础:基本概念、架构属性、架构约束、使用场景、实现框架(服务端、客户端)
  • REST 服务端实践:Spring Boot REST 应用、HATEOAS 应用、文档生成等
  • REST 客户端实践:传统浏览器、Apache HttpClient 、Spring RestTemplate 等相关实践

REST理论基础

前面介绍过Spring的MVC结合view显示数据。那么这些数据除了在WebBrowser中用JavaScript来调用以外,还可以用远程服务器的Java程序、C#程序来调用。也就是说现在的程序不仅在BS中能调用,在CS中同样也能调用,不过你需要借助RestTemplate这个类来完成。RestTemplate有点类似于一个WebService客户端请求的模版,可以调用http请求的WebService,并将结果转换成相应的对象类型。

RPC

Q: 什么是 RPC 呢?

A:RPC 即 Remote Procedure Call,翻译过来是远程服务调用。

Q:RPC 与 REST 又有怎样的联系呢?

A:上一节介绍过 SpringMVC 通过其视图技术可以将数据渲染给前端。那么这些数据除了通过浏览器比如 JS 调用以外,还可以用远程服务器的 Java 程序、C# 程序来调用。也就是说现在的程序不仅在 B/S 架构中能调用,在 C/S 架构中同样也能调用,通常有两种实现方式:

  • 面向服务的架构(SOA)

通常借助 Web Services 来实现。比如 SOAP(传输介质协议)或者 HTTP、SMTP(通讯协议)。

  • 微服务(MSA)

通常借助 REST 来实现。我们知道 REST 常采用的介质是:HTML、JSON、XML 等等。实现原理则是基于 HTTP(通讯协议)。

关于 HTTP 通常有两种版本:一种是 HTTP 1.1,采用短连接 (Keep-Alive);另外一种则是 HTTP/2,采用长连接。

RPC 从技术实现来讲有三种实现方式:

  1. Spring 客户端 : RestTemplate

RestTemplate 有点类似于一个 WebService 客户端请求的模版,可以调用 http 请求的 WebService,并将结果转换成相应的对象类型。

  1. Spring WebMVC

比如使用一个组合注解:@RestController,其效果等同于同时使用了 @Controller + @ResponseBody + @RequestBody 这三个注解。

  1. Spring Cloud

RestTemplate 基础上支持了负载均衡,只需要加上 @LoadBalanced 注解即可。

如果想要了解更多关于 REST 的理论,可以参考 这篇 WIKI。

Cacheability(可缓存性)

与在万维网上一样,客户端和中介可以缓存响应。响应必须隐式或显式地将自己定义为可缓存或不可缓存,以防止客户端响应其他请求而提供陈旧或不适当的数据。管理良好的缓存部分或完全消除了某些客户端-服务器交互,从而进一步提高了可伸缩性和性能。

那么我们该如何理解缓存的响应问题呢?其实在上面提到的注解 @ResponseBody 就可以理解为是响应体(Response Body)。我们知道响应通常分为响应头和响应体两个部分:

参考 HttpEntity 源码,其中包含响应头和响应体:

  public class HttpEntity<T> {
  	...
  	
  	//响应头
  	private final HttpHeaders headers;
  
  	//响应体
  	private final T body;
  	
  	...
  }
  1. 响应头(Headers)

响应头是用来存放元信息(Meta-Data),比如通过设置 Connection 为 Keep-Alive 建立长连接、设置 Accept-Language 会自动识别 Locale 解析语言等等。

Q:那么响应头是如何存储诸多字段对应的功能呢?

A:根据源码分析,请求头通过多值 Map(多值Map是Spring提供的结构)的方式设计(即一个 Key 对应多个 Value)。

参考 HttpHeaders 的源码,其实现了 MultiValueMap:

public class HttpHeaders implements MultiValueMap<String, String>, Serializable {
    ...
}
  1. 响应体(Body/Payload)

通常在 HTTP 实体或 REST 场景下,我们称之为 Body;而在 消息(JMS)、事件、SOAP 这样的场景下我们称之为 Payload。

响应体用来存放业务信息(Business Data)。通常是 HTTP 实体、REST。

HTTP 状态码

可以参考源码:(org.springframework.http.HttpStatus

微服务专题03-REST_第1张图片
关于 http 状态码,常见的有:

状态码 枚举字段 备注
200 OK 请求成功
304 NOT_MODIFIED 如果客户端发送了一个带条件的 GET 请求且该请求已被允许,而文档的内容并没有改变,则服务器应当返回这个状态码。
400 BAD_REQUEST 语义有误,或者请求参数有误。
404 NOT_FOUND 请求失败,请求所希望得到的资源未被在服务器上发现。
500 INTERNAL_SERVER_ERROR 服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。

缓存验证Demo
启动类:

@EnableAutoConfiguration
@ComponentScan(basePackages = "com.test.micro.services.mvc.controller")
public class MvcRestApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(MvcRestApplication.class)
                .run(args);
    }

}

CachedRestController

@Controller
public class CachedRestController {

      //  Spring MVC 返回值处理
      @RequestMapping("/cache") 
      public ResponseEntity<String> cachedHelloWorld(
              @RequestParam(name = "cached", required = false, defaultValue = "false") String cached) {
          if (!"false".equals(cached)) {
              //缓存返回 304
              return new ResponseEntity(HttpStatus.NOT_MODIFIED);
          } else {
              //不缓存返回 200
              return ResponseEntity.ok("Hello,World");
          }
      }
}

代码测试:

首次访问地址:
http://localhost:8080/cache?chched=false

微服务专题03-REST_第2张图片
不加入缓存,http响应码为200

在url控制缓存,第二次请求的地址http://localhost:8080/cache?cached=ture
微服务专题03-REST_第3张图片

加入缓存后,http响应码为304

实验结果分析:

  • 第一次完整请求,获取响应头(200),直接获取。
  • 第二次请求,只读取头信息,响应头(304),客户端(浏览器)取上次 Body 结果。

Uniform interface(统一接口)

URI & URL

URI 与 URL 的区别:

首先从字面上看一下 URI 与 URL 字段的区别:U 指的是统一(Uniform)、R 指的是资源(Resource)。区别点则在于 I 重在鉴别 、而 L 重在定位。

通俗点讲,举一个例子:山东和河南都有一个人叫张三,张三就是所谓的 URI,具体的河南的张三或者山东的张三就是所谓的 URL。

扩展:

URI = scheme:[//authority]path[?query][#fragment] 。

scheme 在 URI 中通常的实现途径为: HTTP、WeChat。而 scheme 在 URL 中一般指的是 protocol 协议。

资源操作

下面说下 restful 的 http 常见的几种方法:

HTTP 动词 对应注解 备注
GET @GetMapping 用于获取资源。
PUT @PutMapping 用于创建、更新资源。
POST @PostMapping 用于创建子资源。
PATCH @PatchMapping 用于创建、更新资源,于 PUT 类似,区别在于 PATCH 代表部分更新。
DELETE @DeleteMapping 删除资源。

关于 PATCH 动词特别说明:

PATCH 存在的限制:Servlet API 没有规定 PATCH ,但是 Spring Web 对其做了扩展:

 public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
    
        ...
    @Override    
    protected void service(HttpServletRequest request, HttpServletResponse response)
    			throws ServletException, IOException {
    
    		HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
    		if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
    			processRequest(request, response);
    		}
    		else {
    			super.service(request, response);
    		}
    	}
        ...
    }

之前在第一节我们讨论了关于 Spring 注解编程模型之模式注解(注解派生性)的问题。这里说一下 Spring 注解编程模型之注解属性别名和覆盖的相关问题。

在上面的提到的 @GetMapping 等这类的注解其实它们的元注解就是 @RequestMapping,即 @RequestMapping 元标注了 @GetMapping,只不过 @GetMapping 指定了当前请求的方式为 Get。我们看源码就可以发现:

    // 注解“派生性”
    @RequestMapping(method = RequestMethod.GET) 
    public @interface GetMapping {
        ...
        // 注解别名
        @AliasFor(annotation = RequestMapping.class) 
    	String name() default "";
        ...
    }

Tips:关于注解属性别名和覆盖的特性,是 Spring Framework 4.2 版本开始引入的,Spring Boot 1.3 之后才可以使用,但却是 Spring Boot 对其加以发展。

这里的 @AliasFor 只能标注在目标注解的属性,所 annotation() 的注解必须是元注解,该注解 attribute() 必须元注解的属性。

举例说明:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.OPTIONS)
public @interface OptionsMapping {
    
    // 指定 RequestMethod 的属性
    @AliasFor(annotation = RequestMapping.class) 
    // 不加的话,只是代表自己
    String name() default ""; 

}

这里我模拟 @GetMapping 自定义了一个 @OptionsMapping 注解。值得注意的是:如果不增加元注解 @RequestMapping 的话,会报错,另外需要通过 @AliasFor 重新定义属性,这样就通过注解属性别名和覆盖就完成了一个自定义的注解。

然后再看看SpringBootApplication注解:
微服务专题03-REST_第4张图片
这也是为什么我们可以在启动 SpringBoot 项目直接使用 @EnableAutoConfiguration 的原因,就如同本节缓存验证 Demo 的启动类一样。值得注意的是 @EnableAutoConfiguration 来自于 SpringFramework,而 @SpringBootApplication 则来自于 SpringBoot 的 jar 包,说白了,就是 SpringBoot 对于 Spring 进一步的封装,所以为什么说学好 SpringBoot 要学好 Spring,原因就在于此!
微服务专题03-REST_第5张图片

自定义注解

基于以上的知识点,我们来模仿springboot自定义一个注解:
比如既实现注入bean也同时实现事务的注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Service // 它是 @Service 组件
@Transactional // 它是事务注解
public @interface TransactionalService { //  @Service + @Transactional

    @AliasFor(annotation = Service.class)
    String value(); // 服务名称

    @AliasFor(annotation = Transactional.class,attribute = "value")
    String txName();
}

在service层调用:

@TransactionalService(value = "echoService-2020", txName = "myTxName") // @Service Bean + @Transactional
// 定义它的 Bean 名称
public class EchoService {

    public void echo(String message) {
        System.out.println(message);
    }
}

测试类:

@ComponentScan(basePackages = "com.test.micro.services.mvc.service")
@EnableTransactionManagement
public class SpringApplication {

    @Component("myTxName")
    public static class MyPlatformTransactionManager implements PlatformTransactionManager {

        @Override
        public TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
            return new DefaultTransactionStatus(
                    null, true, true,
                    definition.isReadOnly(), true, null
            );
        }

        @Override
        public void commit(TransactionStatus status) throws TransactionException {
            System.out.println("Commit()....");
        }

        @Override
        public void rollback(TransactionStatus status) throws TransactionException {
            System.out.println("rollback()....");
        }
    }

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        // 注册 SpringApplication 扫描  com.test.micro.services.mvc.service
        context.register(SpringApplication.class);

        context.refresh(); // 启动

        context.getBeansOfType(EchoService.class).forEach((beanName, bean) -> {
            System.err.println("Bean Name : " + beanName + " , Bean : " + bean);

            bean.echo("Hello,World");
        });

        context.close(); // 关闭
    }
}

测试结果:
微服务专题03-REST_第6张图片
说明此注解生效,注入了service也实现了事务!

自描述消息

所谓的自我描述的信息,指的是每个消息都包含足够的信息来描述如何处理该消息,方便代码去处理和解析其中的内容。

SpringBoot 中默认使用 Jackson2 序列化方式,其中媒体类型:application/json,它的处理类 MappingJackson2HttpMessageConverter,所以浏览器访问显示 json。你也可以自己去重写 WebMvcConfigurer 实现不同的序列化规则、编码规则、媒体类型等配置。

接下来我们看看 SpringBoot 对于 REST 的支持是通过哪些途径实现的?

注解驱动

常见的比如 @RequestBody@ResponseBody,前面提到了,默认的 JSON 协议通过 MappingJackson2HttpMessageConverter 处理,而 TEXT 协议通过 StringHttpMessageConverter 处理。返回值处理请参考源码 RequestResponseBodyMethodProcessor

接口编程

ResponseEntityRequestEntity 则都继承于 HttpEntity。HttpEntity 就是我们前面提到的原始的 HTTP 信息,包含请求头和请求体。说白了,无论Request 和 Response 其实最关心的就是请求头和请求体。返回值处理请参考源码 HttpEntityMethodProcessor

媒体类型(MediaType

常见如 application/json;charset=UTF-8 ,所在源码路径:org.springframework.http.MediaType#APPLICATION_JSON_UTF8_VALUE

前面多次提到的 HTTP 消息转换器(HttpMessageConverter)同样包含这样的属性:application/json,所在源码路径 MappingJackson2HttpMessageConverter。text/html,所在源码路径 StringHttpMessageConverter

@EnableWebMvc 源码导读

@EnableWebMvc 是使用 Java 注解快捷配置 Spring WebMVC 的一个注解。在使用该注解后配置一个继承于 WebMvcConfigurerAdapter 的配置类即可配置好Spring WebMVC。

首先需要导入 DelegatingWebMvcConfiguration(配置 Class),然后注册 WebMvcConfigurer,过程中需要,装配各种 Spring MVC 需要的 Bean 以及注解驱动扩展点:

  • HandlerMethodArgumentResolver
  • HandlerMethodReturnValueHandler
  • @RequestBody@ResponseBody 实现类:即 RequestResponseBodyMethodProcessorHttpEntityMethodProcessor

后记

Q: 前后端分离和服务端渲染的区别

A: 服务端渲染,数据计算 + HTML 渲染均有服务端完成。

前后端:数据计算有服务端提供,JSON Web 端点(REST Web 接口),HTML 主要前端 JS 完成,以React、Vue.js(前端模板引擎)

本节示例代码:https://github.com/harrypottry/microservices-project/spring-mvc-rest

更多架构知识,欢迎关注本套Java系列文章:Java架构师成长之路

你可能感兴趣的:(微服务专题)