RESTful风格的应用
- 一.开发第一个RESTful应用
- RESTful开发规范
- RESTful命名要求
- 开发第一个RESTful应用
- 二.RESTful基本使用
- RestController注解
- 路径变量
- 简单请求与非简单请求
- JSON序列化
- 三.跨域问题
- 浏览器的同源策略
- CrossOrigin注解解决跨域访问
一.开发第一个RESTful应用
本节讲解一个全新的话题,RESTful开发风格。 REST :全称Representational State Transfer(表现层状态转换),资源在网络中,以某种表现形式进行状态转移。听起来很难理解,其实说白了就是在我们web环境下,如果你要获取某个图片,js,网页这些资源的时候,就要以url的形式进行表现。我们访问一个图片的网址,那这个资源返回的就自然是一张图片,如果访问的是一个CSS,那返回的就是一个CSS。好像这种设计理念对于我们的web应用来说是在再基础不过的东西。其实这是rest给我们提出来的一个设计理念,在web环境以URL的方式,来进行资源的传递。那么基于这种REST的理念,注意,是理念,不是具体的实现。
RESTful是基于REST理念的一套开发风格,是具体的开发规则。 下面通过一个图来进行解释:
在这个图的最左侧,我们的客户端已经不再是标准的浏览器了,而是包含了像iPhone和安卓系统里所运行的小程序和app,都是可以作为客户端来使用的。而RESTful开发风格下,我们也并不拘泥于客户端必须是浏览器。那客户端和服务器之间如何交互呢?在这里,我打个比方。比如iPhone中有一个小程序向这个URL发送了一个请求,而这个请求被发送到了web端的服务器,那请求在被处理了以后,关键的区分来了,作为服务器端返回的已经不再是某一个HTML的文本,而是像json或是xml这样的数据。作为RESTful最典型的特征就是,我们服务器端只返回数据 ,这种数据以json或者是xml的方式进行体现。同时返回的数据要求不包含任何与展现相关的内容。当这些数据被送回到客户端以后,再由客户端对这些数据进行渲染和展现。比如我们PC端的浏览器接收到这个JSON以后,可能是以一个表格的形式在浏览器中进行展现,而iPhone或者安卓这种移动端的小屏幕的话,它可能会以滑动列表的形式进行展现。那如何展现呢?这就是客户端的事情了。作为服务器,我不管你客户端使用的是小程序,app还是浏览器,只管专注产生数据就行了,至于数据以什么形式展现出来,那是客户端的事情。这样做最大的好处就是我们开发服务器的后端工程师,只用专注数据,不用关注任何展现。而前端的每一个工程师也不用去关注后台是如何产生数据的。只需要拿到这个字符串进行解析就可以了。在开发的过程中,前端的工程师和后端的工程师可以同步进行,只要我们约定好传递字符串的格式和url就可以了。通过基于RESTful开发风格所编写的程序在行业中还有一个名词叫做前后端分离。前端只负责界面开发,后端只需要专注于业务逻辑就可以了。
RESTful开发规范
1.使用URL作为用户交互入口。
2.明确的语义规范(GET | POST | PUT | DELETE) 这里的语义规范是指在http发送请求的时候,例如get请求或post请求他们自己所实现的含义是有所不同的。在我们日常开发中最常用的http发送的方式有四种GET 、 POST 、PUT 、DELETE。但是后两者我们几乎没有见过,那是为什么呢?是因为在web环境下,只支持get或post请求,不支持put和delete请求。所以我们之前写代码看不到这两种请求。但是看不到,并不代表没有。作为REST在进行语义规范定义的时候,get、post、put、delete其实分别对应了查询操作、新增操作、更新操作、删除操作。也就是说,同一个URL在向服务器发送请求的时候,使用了不同的请求方式,那他在服务器端进行的处理是不一样的。例如你发送一个get请求到服务器端,那程序按照RESTful开发规范,就必须只是一个查询操作,返回请求所对应的数据。那如果是post请求,post对应的是新增操作,那在服务器端的controller中,就要完成对某个数据的新增操作。而put就是数据的更新操作,delete是删除操作。通过遵循RESTful开发规范,当我们看到这个请求的类型的时候,我们就找到要做增删改查的哪一种了。
3.只返回数据(json | xml) ,不包含任何展现。 也就是指在我们服务器产生的数据通常是以json字符串或者xml字符串。日常开发中,优先推荐返回json数据,因为json数据无论是从可读性,还是解析的角度都要比xml简单得多。并且json天然地被JavaScript支持,使用起来更方便。因此主流地web框架,比如说SpringBoot , SpringMVC它都是默认对json提供了良好的支持。
下面举一个例子来举例一下,RESTful怎么写是对的,怎么写是错的。
RESTful命名要求
菜鸟教程也有关于RESTful的相关文章:https://www.runoob.com/w3cnote/restful-architecture.html
开发第一个RESTful应用
创建一个普通的maven项目,按照以前的规范导入web模块后,然后在pom.xml中导入springmvc的依赖:
代码语言:javascript复制 <dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.13</version>
</dependency>
然后配置web.xml。在里面配置DispatcherServlet和CharacterEncodingFilter,代码如下:
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!--配置DispatcherServlet-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--加载applicationContext.xml-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</init-param>
<!--在web应用启动时,自动创建Spring IoC容器,并初始化DispatcherServlet-->
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<!--"/"代表要拦截所有的请求-->
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--将请求的字符集转换为UTF-8,CharacterEncodingFilter比DispatcherServlet优先执行-->
<filter>
<filter-name>characterFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterFilter</filter-name>
<!--对所有的请求进行过滤-->
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
配置applicationContext.xml:
代码语言:javascript复制<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd">
<!--自动扫描组件,自动扫描符合条件的bean-->
<context:component-scan base-package="com.haiexijun.restful"></context:component-scan>
<!--启用Spring MVC的注解开发模式-->
<mvc:annotation-driven>
<!--配置消息转换器,让响应输出到浏览器以后,以text/html;charset=utf-8输出,解决html里面的中文乱码问题-->
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/html;charset=utf-8</value>
<value>application/json;charset=utf-8</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<!--将图片/JS/CSS等静态资源排除在外,可提高执行效率-->
<mvc:default-servlet-handler/>
</beans>
在com.haiexijun.restful包下创建controller控制器包,并在包下面创建一个全新的java类RestfulController。然后书写如下的代码:
代码语言:javascript复制package com.haiexijun.restful.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/restful")
public class RestfulController {
@GetMapping("request")
@ResponseBody
public String doGetRequest(){
//注意这里模拟返回一个json字符串
return "{"message":"返回查询结果"}";
}
}
到这里,一个RESTful风格的程序就写好了。你可能会有疑问,这不就是我之前学习过的东西吗?他怎么就是RESTful呢?其实,restful解释一种编码的风格,不是一种新的技术。作为restful我们要求,url所有的部分都是名词,除此以外返回的数据也要求是一个json,或者是一个xml格式的数据。同时get、post、put、delete这四种请求也有不同的涵义。上面定义get请求,我们返回的就是查询的结果。
运行结果如下:
html可以通过ajax技术来使用这个json数据。下面继续来开发restful。来模拟一下客户端页面与restful交互的过程.
二.RESTful基本使用
上一节开发了一个Controller,实现了标准的RESTful风格,本节就来开发html的客户端与服务器端的RESTful进行交互。
返回刚才的工程,在webapp目录下放入jquery.js 文件。等下要用到它来完成Ajax的请求。然后创建一个标准的html页面,这里我叫做index.html。index.html编写如下代码?
代码语言:javascript复制<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>RESTful</title>
<script src="jquery.js"></script>
<script>
$(function (){
$("#btnGet").click(function (){
$.ajax({
url:"/restful/request",
type:"get",
dataType:"json",
success:function (data){
$("#message").text(data.message)
}
})
})
})
</script>
</head>
<body>
<input type="button" id="btnGet" value="发送Get请求">
<h1 id="message"></h1>
</body>
</html>
运行后点击按钮获取数据:
下面在程序中体验一下post等其他请求,在controller中添加如下代码: 这里mapping都为request其实问题不大,因为请求的方式不同。
代码语言:javascript复制 @PostMapping("/request")
@ResponseBody
public String doPostRequest(){
return "{"message":"数据新建成功"}";
}
@PutMapping("/request")
@ResponseBody
public String doPutRequest(){
return "{"message":"数据更新成功"}";
}
@DeleteMapping("/request")
@ResponseBody
public String doDeleteRequest(){
return "{"message":"数据删除成功"}";
}
当然,这里只是为了方便学习写成这样的,真正开发肯定不是这样写的。在html中更改ajax的http请求类型就可以了,这里不进行测试了。
RestController注解
这两者究竟是什么呢?下面通过程序一演示就明白了。
首先回到之前的项目中,在上面我们在controller中写入查询、新增、修改和删除这四种操作。但是发现一件特别麻烦的事情,每一次在书写方法以后,都需要在每一个方法上写@ResponseBody,这样返回的字符串才能被正常地输出到响应。所以在Spring4以后。体提供了一个新的spring注解,叫做@RestController ,把它替换原来的@Controller注解写在类名上即可,这个注解的作用就是,只要一写上以后,默认当前方法返回的都是rest形式的数据。使用了这个注解以后,我们最大的好处就是不需要在每个方法上添加@ResponseBody了,它默认就会将这个字符串向请求中进行输出,帮助我们简化开发。
路径变量
我们见过 POST/article/1 这样的一种uri的书写形式,表示创建一个ID值为1的文章。像这种url,id这个位置其实是灵活的,是变化的,这里可能是1,之后就是3等了。这种放在uri中的变量,就成为路径变量。 在restful风格下,这种路径变量的使用是很普遍的。就拿当前的这个例子来说,比如我要创建一个全新的请求,这个请求的ID值假设是100的话,可能我们会书写成POST/restful/request/100 ,那在我们服务器端这么接收到这个100呢?它并不是我们请求的参数,而是我们uri中的一部分啊。好在Spring MVC为我们提供了路径变量,我们只需要在这个@xxxMapping("/request")里面的url后面添加一个{},并给一个路径变量名字就可以了。比如@xxxMapping("/request/{rid}") ,那在我们程序中如何接收呢?其实也很简单,在方法中创建一个参数,比如Integer requestId ,然后在参数前添加@PathVariable注解,注解里面传入路径变量的名字rid。
示例如下:
代码语言:javascript复制 @GetMapping("/request/{rid}")
public String doGetRequest(@PathVariable("rid") Integer requestId){
return "{"message":"返回查询结果" requestId ""}";
}
更改html的ajax请求:
代码语言:javascript复制 <script>
$(function (){
$("#btnGet").click(function (){
$.ajax({
url:"http://localhost:8888/restful/request/100",
type:"get",
dataType:"json",
success:function (data){
$("#message").text(data.message data.id)
}
})
})
})
</script>
运行如下:
简单请求与非简单请求
简单请求是指标准结构的HTTP请求,对应GET/POST请求。是我们以前在html中最常使用的标准结构的http请求。 非简单请求是指复杂要求的HTTP请求,指PUT/DELETE请求、扩展标准请求。 简单请求与非简单请求在数据的结构上,几乎是一致的,只是在数据的内容上会略有不同。PUT和DELETE是特殊的请求发送方式,而扩展标准请求则自定义了额外的请求头。两者最大的区别是非简单请求发送前,需要发送预检请求。
这是一个非简单请求的发送过程,在原始的情况下,如果是简单请求,直接把请求发过来,由服务器处理就完事了。但是如果是非简单请求的话,它首先要发送一个预检请求,预检请求的作用是让服务器返回当前这个请求能不能够被正常地处理,如果服务器返回能进行处理,之后再由浏览器发送实际的请求给服务器进行处理。
下面我们通过程序来演示一下这个案例,回到上面的项目,重新编写controller里面的代码:
代码语言:javascript复制package com.haiexijun.restful.controller;
import com.haiexijun.restful.entity.Person;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/restful")
public class RestfulController {
@GetMapping("/request")
public String doGetRequest(Person person){
System.out.println(person.getName() ":" person.getAge());
//注意这里模拟返回一个json字符串
return "{"message":"返回查询结果"}";
}
@PostMapping("/request")
public String doPostRequest(Person person){
System.out.println(person.getName() ":" person.getAge());
return "{"message":"数据新建成功"}";
}
@PutMapping("/request")
public String doPutRequest(Person person){
System.out.println(person.getName() ":" person.getAge());
return "{"message":"数据更新成功"}";
}
@DeleteMapping("/request")
public String doDeleteRequest(){
return "{"message":"数据删除成功"}";
}
}
我们用Person对象来接收数据,故要定义一个Person的javabean:
代码语言:javascript复制package com.haiexijun.restful.entity;
public class Person {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
然后重新编写html,写几个按钮,分别来对比post请求和put请求:
代码语言:javascript复制<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>RESTful</title>
<script src="jquery.js"></script>
<script>
$(function (){
$("#btnGet").click(function (){
$.ajax({
url:"/restful/request?name=lily&age=23",
type:"get",
dataType:"json",
success:function (data){
$("#message").text(data.message)
}
})
})
$("#btnPost").click(function (){
$.ajax({
url:"/restful/request",
data:"name=lily&age=23",
type:"post",
dataType:"json",
success:function (data){
$("#message").text(data.message)
}
})
})
$("#btnPut").click(function (){
$.ajax({
url:"/restful/request?name=lily&age=23",
type:"put",
dataType:"json",
success:function (data){
$("#message").text(data.message)
}
})
})
$("#btnDelete").click(function (){
$.ajax({
url:"/restful/request",
type:"delete",
dataType:"json",
success:function (data){
$("#message").text(data.message)
}
})
})
})
</script>
</head>
<body>
<input type="button" id="btnGet" value="发送Get请求">
<input type="button" id="btnPost" value="发送Post请求">
<input type="button" id="btnPut" value="发送Put请求">
<input type="button" id="btnDelete" value="发送Delete请求">
<h1 id="message"></h1>
</body>
</html>
我们运行后,点击第一个按钮get请求和第二个按钮post请求后发送简单请求,发现可以完成请求:
但是,当我们点击Put请求的时候,就不能完成请求了。如下,put并没有接收到实际的数据,控制台打印null:
那这又是为什么呢?这里就涉及到一个历史问题了。作为最早的springMVC是为我们网页服务的。默认网页在表单提交的时候只支持GET和 POST这两种请求,对于PUT和DELETE是不支持的。但是随着技术的演进,put和delete作为springmvc必须要考虑的。但又不能把put和delete请求的处理方式强塞进原有的代码中,所以springmvc做了一个折中的方案,作为PUT和DELETE这两种非简单请求,springmvc提供了一个额外的表单内容过滤器来对put和delete进行处理。
那它的具体写法是: 打开web.xml文件,在这个xml文件中增加一个filter。代码如下:
代码语言:javascript复制 <!--表单内容过滤器,用于对非简单请求进行额外处理-->
<filter>
<filter-name>formContentFilter</filter-name>
<filter-class>org.springframework.web.filter.FormContentFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>formContentFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
然后重启运行一下,所有请求都能正常处理了。
JSON序列化
在学习JSON序列化时,先要导入一个maven依赖Jackson,在中央仓库搜索jackson-core和jackson-databind和jackson-annotations,jackson是目前世界上使用范围最广,效率最高的JSON序列化组件。但是我们最好还是选择2.9.4版本以后的版本,因为之前的版本有严重的安全隐患。
代码语言:javascript复制 <!--jackson的核心包-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.13.2</version>
</dependency>
<!--jackson数据绑定包-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.2</version>
</dependency>
<!--jackson注解的包-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.13.2</version>
</dependency>
那jackson如何在springmvc里面来使用呢?我们还是通过案例来书写一下。这里打开之前写好的控制器RestfulController,在这控制器中我们增加一个方法:
代码语言:javascript复制 @GetMapping("/person")
public Person findByPersonId(Integer id){
Person p = new Person();
if (id==1){
p.setAge(23);
p.setName("lily");
}else if (id==2){
p.setAge(22);
p.setName("smith");
}
return p;
}
在这里返回的是Person对象而不是string或者ModelAndView呢?因为刚才我们配置了jackson,所以jackson会自动帮我们进行序列化输出。这解决了我们手动拼接字符串时的麻烦。
我们运行项目,结果如下:
下面再来补充一个在实际应用中的特殊场景,比如我们现在查询的不是单个对象,而是多个对象,该如何返回呢?下面我们示意性的写一下:
代码语言:javascript复制 @GetMapping("/persons")
public List<Person> findPersons(){
List<Person> list=new ArrayList<Person>();
Person p1=new Person();
p1.setAge(22);
p1.setName("smith");
Person p2=new Person();
p2.setAge(23);
p2.setName("lily");
list.add(p1);
list.add(p2);
return list;
}
下面是浏览器访问的运行结果:
下面再补充一个小知识点,如果返回的对象中有日期信息呢,那返回的json对象其实对于时间日期信息返回的是一个到1970年的时间戳。比如我们在Person类里面添加一个birthday。如果要让Jackson对时间进行格式化,就要在该属性前添加一个**@JsonFormat**注解,里面用pattern设置格式化的格式,timezone设置时区为东八区。
代码语言:javascript复制 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT 8")
private Date birthday;
更改一下方法:
代码语言:javascript复制 @GetMapping("/persons")
public List<Person> findPersons(){
List<Person> list=new ArrayList<Person>();
Person p1=new Person();
p1.setAge(22);
p1.setName("smith");
p1.setBirthday(new Date());
Person p2=new Person();
p2.setAge(23);
p2.setName("lily");
p2.setBirthday(new Date());
list.add(p1);
list.add(p2);
return list;
}
浏览器访问的结果:
三.跨域问题
浏览器的同源策略
本节来聊一个在restful中必须要考虑的问题,就是跨域访问。那什么是跨域访问呢?为什么要强调浏览器的跨域访问呢?这其实,跨域访问的根源是来自于浏览器的同源策略。
浏览器的同源策略是指阻止一个域加载的脚本去获取另外一个域上的资源。听起来有一点晦涩,说白了,我们有两个网站,一个是网站A,一个是网站B。他们有不同的域名在不同的服务器上。如果A的某一个页面向B的某个URL发送了AJAX请求的话,就会因为同源策略被阻止。原因很简单,就是浏览器为了保证我们的网站足够的安全。如果没有同源策略的保护,那任何一个网站都可以向其他网站发起请求。只要协议,域名,端口有任何一个不同,都被当做是不同的域。 浏览器的console看到Access-Control-Allow-Origin就代表跨域了,请求得到的结果并不会被浏览器处理。
下面我们看几个例子:
在html里面的有一些标签是不受同源策略约束的,HTML中允许跨域的标签:
CrossOrigin注解解决跨域访问
首先我们要了解一个名词,如果要涉及到跨域访问,肯定会经常看到一个名词叫CORS ,翻译过来就是跨域资源共享的意思。
说起CORS就要说到它的底层原理了,CORS是一种机制,使用额外的HTTP头通知浏览器可以访问其他域。 URL响应头包含Access-Control-*指明请求允许跨域。 但这个响应头并不是我们自己随随便便就可以加上的,这是要远程服务器对应的资源进行相应的授权,才允许访问。
那Springmvc里如何做到跨域访问呢?方法主要有两种: 第一种是在我么的类中,使用@CrossOrigin(origins={“允许跨域的全域名”,“允许跨域的全域名”} ,maxAge=秒数) 这个注解来说明当前controller所映射的URL允许被跨域访问。这是局部的处理,当然了,这个注解,只是在当前的controller里面生效。如果我们系统中有大量的controller要考虑跨域的问题。如果要允许所有的跨域请求都允许访问的话,写成:@CrossOrigin(origins={" * "}) ,但是并不推荐这样去使用。
代码语言:javascript复制@RestController
@RequestMapping("/restful")
@CrossOrigin(origins={"http://localhost:8080"},maxAge=3600)
public class RestfulController {
········省略·····
·······省略·····
}
上的设置了maxAge为3600又是什么意思呢?前面我们知道,PUT和DELETE都是非简单请求,那非简单请求在发送的时候,首先要发送一个预检请求,来向服务起检查当前的请求是否允许被访问呢?如果允许,我们就发送实际的请求,如果不允许,当前的操作就会被中断。但是,这又会产生一个新的问题。作为PUT和DELETE这种非监督请求,在每一次发送的时候,都其实有两个请求。这必然会增加服务器的压力。而且作为服务器端,预检请求授权的逻辑是不会轻易地改变的。所以刚才的maxAge就起到作用了。maxAge将预检请求的结果进行缓存,设置了3600秒,也就是一小时。在一小时的时间内,同样的PUT请求再次发送的时候就不需要再发起预检请求处理了。直接发送实际请求。
这时可以使用第二种方式,在配置文件中,使用<mvc:cors>这个标签一次性的全局配置。 这样可以一劳永逸。
代码语言:javascript复制 <mvc:cors>
<mvc:mapping path="/restful/**" allowed-origins="http://localhost:8080,http://localhost:9999" max-age="3600"/>
</mvc:cors>
path指定要跨域的资源的路径,上面的/restful/**就是restful下所有的controller。 allowed-origins指定允许跨域的域名,多个域名用逗号隔开。maxAge同上。
注意:如果我们既配置了全局的,又配置了注解的,springmvc会以注解的配置为准。