前面的章节我们讲了Spring Web MVC 。本节,继续微服务专题的内容分享,共计16小节,分别是:
本节内容重点为:
前面介绍过Spring的MVC结合view显示数据。那么这些数据除了在WebBrowser中用JavaScript来调用以外,还可以用远程服务器的Java程序、C#程序来调用。也就是说现在的程序不仅在BS中能调用,在CS中同样也能调用,不过你需要借助RestTemplate这个类来完成。RestTemplate有点类似于一个WebService客户端请求的模版,可以调用http请求的WebService,并将结果转换成相应的对象类型。
Q: 什么是 RPC 呢?
A:RPC 即 Remote Procedure Call,翻译过来是远程服务调用。
Q:RPC 与 REST 又有怎样的联系呢?
A:上一节介绍过 SpringMVC 通过其视图技术可以将数据渲染给前端。那么这些数据除了通过浏览器比如 JS 调用以外,还可以用远程服务器的 Java 程序、C# 程序来调用。也就是说现在的程序不仅在 B/S 架构中能调用,在 C/S 架构中同样也能调用,通常有两种实现方式:
通常借助 Web Services 来实现。比如 SOAP(传输介质协议)或者 HTTP、SMTP(通讯协议)。
通常借助 REST 来实现。我们知道 REST 常采用的介质是:HTML、JSON、XML 等等。实现原理则是基于 HTTP(通讯协议)。
关于 HTTP 通常有两种版本:一种是 HTTP 1.1,采用短连接 (Keep-Alive);另外一种则是 HTTP/2,采用长连接。
RPC 从技术实现来讲有三种实现方式:
RestTemplate 有点类似于一个 WebService 客户端请求的模版,可以调用 http 请求的 WebService,并将结果转换成相应的对象类型。
比如使用一个组合注解:@RestController
,其效果等同于同时使用了 @Controller
+ @ResponseBody
+ @RequestBody
这三个注解。
在 RestTemplate
基础上支持了负载均衡,只需要加上 @LoadBalanced
注解即可。
如果想要了解更多关于 REST 的理论,可以参考 这篇 WIKI。
与在万维网上一样,客户端和中介可以缓存响应。响应必须隐式或显式地将自己定义为可缓存或不可缓存,以防止客户端响应其他请求而提供陈旧或不适当的数据。管理良好的缓存部分或完全消除了某些客户端-服务器交互,从而进一步提高了可伸缩性和性能。
那么我们该如何理解缓存的响应问题呢?其实在上面提到的注解 @ResponseBody
就可以理解为是响应体(Response Body)。我们知道响应通常分为响应头和响应体两个部分:
参考 HttpEntity 源码,其中包含响应头和响应体:
public class HttpEntity<T> {
...
//响应头
private final HttpHeaders headers;
//响应体
private final T body;
...
}
响应头是用来存放元信息(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 {
...
}
通常在 HTTP 实体或 REST 场景下,我们称之为 Body;而在 消息(JMS)、事件、SOAP 这样的场景下我们称之为 Payload。
响应体用来存放业务信息(Business Data)。通常是 HTTP 实体、REST。
可以参考源码:(org.springframework.http.HttpStatus
)
状态码 | 枚举字段 | 备注 |
---|---|---|
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
在url控制缓存,第二次请求的地址http://localhost:8080/cache?cached=ture
加入缓存后,http响应码为304
实验结果分析:
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注解:
这也是为什么我们可以在启动 SpringBoot 项目直接使用 @EnableAutoConfiguration
的原因,就如同本节缓存验证 Demo 的启动类一样。值得注意的是 @EnableAutoConfiguration
来自于 SpringFramework,而 @SpringBootApplication
则来自于 SpringBoot 的 jar 包,说白了,就是 SpringBoot 对于 Spring 进一步的封装,所以为什么说学好 SpringBoot 要学好 Spring,原因就在于此!
基于以上的知识点,我们来模仿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(); // 关闭
}
}
测试结果:
说明此注解生效,注入了service也实现了事务!
所谓的自我描述的信息,指的是每个消息都包含足够的信息来描述如何处理该消息,方便代码去处理和解析其中的内容。
SpringBoot 中默认使用 Jackson2 序列化方式,其中媒体类型:application/json,它的处理类 MappingJackson2HttpMessageConverter,所以浏览器访问显示 json。你也可以自己去重写 WebMvcConfigurer
实现不同的序列化规则、编码规则、媒体类型等配置。
接下来我们看看 SpringBoot 对于 REST 的支持是通过哪些途径实现的?
常见的比如 @RequestBody
、@ResponseBody
,前面提到了,默认的 JSON 协议通过 MappingJackson2HttpMessageConverter
处理,而 TEXT 协议通过 StringHttpMessageConverter
处理。返回值处理请参考源码 RequestResponseBodyMethodProcessor
。
ResponseEntity
与 RequestEntity
则都继承于 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
实现类:即 RequestResponseBodyMethodProcessor
和 HttpEntityMethodProcessor
。Q: 前后端分离和服务端渲染的区别
A: 服务端渲染,数据计算 + HTML 渲染均有服务端完成。
前后端:数据计算有服务端提供,JSON Web 端点(REST Web 接口),HTML 主要前端 JS 完成,以React、Vue.js(前端模板引擎)
本节示例代码:https://github.com/harrypottry/microservices-project/spring-mvc-rest
更多架构知识,欢迎关注本套Java系列文章:Java架构师成长之路