认SpringBoot中REST风格
学习目标
1、关于REST
2、Spring MVC开发REST风格
3、客户端请求RestTemplate
一、关于REST
REST(Representational State Transfer),是由Fielding在2000年的博士论文中提出来的。它可以翻译为表现层状态转换。
它有如下三个主要名词:
资源:它是一个具体存在的对象,可以是系统权限用户、角色菜单、文本、图片……,可以由一个URI来指向它,每一个资源对应一个特定的URI。要获取资源则访问它对应的URI即可。在REST中,URI也可以称为端点(End Point)。
表现层:有了资源还需要确定如何表现这个资源。现在互联网开发中,JSON已存为一种最为常用的表现形式。
状态转换:随着系统的运行资源会有一些变化,一个资源会经历创建(create)、访问(view)、修改(update)和删除(delete)的过程。
通过上同的这些REST风格架构可的特点主要有如下:
服务器存在一系列的资源,每个资源都通过唯一的URI进行标识
客户端与服务器之间可以相互传递资源,而资源会以某种表现层来展示
客户端通过HTTP协议所定义的动作对资源进行操作,用以来实现资源的状态转换
资源的状态转换对应HTTP的动作
GET(view):访问服务器资源(一个或多个)
POST(create):提交服务器资源信息,创建新的资源
PUT(update):修改服务器存在的资源,使用PUT时需要把资源的所有属性一并提交
PATCH(update):修改服务器已存在资源,使用PATCH时需要把部分资源属性提交(不常用)
DELETE(delete):从服务器将资源删除
除了上述5个,HTTP协议中还有两个不常用的动作
HEAD:获取资源元数据(Content-type)
OPTIONS:提供资源可供客户端修改的属性信息
常见的REST风格URI
GET /user/{id} 获取用户信息
GET /user/{userName}/{note} 获取多个用户信息
POST /user/{userName}/{sex}/{note} 创建用户
PUT /user/{id}/{userName}/{sex}/{note} 修改用户全部属性
PATCH /user/{id}/{userName} 修改用户部分属性
在上面的URI中并没有出现动词,参数则主要通过URI设计去获取,对于参数量过多时可以考虑使用JSON作为参数来传递。
二、Spring MVC开发REST风格
Spring MVC整合REST
为了更加便捷地支持REST风格,Spring 4.3之后除了@RequetMapping外还可以使用如下5种注解
@GetMapping:对应HTTP的GET请求,获取资源
@PostMapping:对应HTTP的POST请求,创建资源
@PutMapping:对应HTTP的PUT请求,提交所有资源属性以修改资源
@PatchMapping:对应HTTP的PATCH请求,提交资源部分修改的属性
@DeleteMapping:对应HTTP的DELETE请求,删除服务器端的资源
在REST风格的设计中,如果是简单的参数,一般会通过URL直接传递,Spring MVC中可以使用@PathVariable进行获取,这样就可以满足REST风格传递参数的要求,如果传递的参数复杂可以考虑使用请求体JSON的方式提交给服务器,使用@RequestBody把JSON数据集转换为Java对象。
@RequestMapping、@GetMapping等注解可以把uri定位到控制器方法上
@PathVariable可以把uri地址的参数获取到
@RequestBody可以把请求体为JSON的数据转换为复杂的Java对象
经过上面的注解,请求到达控制器后,可以根据获取的参数来处理对应的逻辑,最终得到后台数据准备渲染给请求。
新增一个Spring Boot的Maven项目,在pom.xml中导入相关的依赖,pom.xml的内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.xiaoxie</groupId>
<artifactId>SpringBoot_REST</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- AOP依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Web开发包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- jstl,jsp依赖 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
<!-- mysql及jdbc依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--Druid依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!-- mybatis依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
</dependencies>
<!-- 引入相关插件 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
在src/main下新增web目录:webapp,同时在其它新增WEB-INF目录以及web.xml
上面的这个添加过程在Project Structure--->Modules中进行新增
在resources下新增相应的mybatis的mapper配置文件的目录:resources/mybatis/mapper
resources下新增Spring的配置文件 application.properties
#Spring MVC的视图解析配置
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
#datasource配置
#spring.datasource.druid.db-type=mysql #这里可以不用配置会根据连接url来自动识别
#spring.datasource.druid.driver-class-name=org.gjt.mm.mysql.Driver #这里可以不用配置会根据连接url自动识别
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
#连接信息
spring.datasource.driver-class-name=org.gjt.mm.mysql.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test
spring.datasource.username=root
spring.datasource.password=root
#连接池配置
spring.datasource.druid.initial-size=15
spring.datasource.druid.max-active=100
spring.datasource.druid.min-idle=15
spring.datasource.druid.max-wait=60000
spring.datasource.druid.keep-alive=true
#默认事务隔离级别“读写提交”
spring.datasource.druid.default-transaction-isolation=2
#Mybatis配置
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
mybatis.type-aliases-package=com.xiaoxie.pojo
#日志配置为DEBUG级别
logging.level.root=DEBUG
logging.level.org.springframework=DEBUG
logging.level.org.org.mybatis=DEBUG
新增一个实体类:com.xiaoxie.pojo.User
package com.xiaoxie.pojo;
import com.xiaoxie.enums.SexEnum;
import org.apache.ibatis.type.Alias;
@Alias("user")
public class User {
private Long id;
private String userName;
private SexEnum sex = null;
private String note;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public SexEnum getSex() {
return sex;
}
public void setSex(SexEnum sex) {
this.sex = sex;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
}
上面这个实体与数据库中的表实体进行对应,注意这里使用了@Alias("user")注解,它是对实体做了别名的定义
新增增一个实体的转换类:com.xiaoxie.vo.UserVo
package com.xiaoxie.vo;
/**视图转换对象*/
public class UserVo {
private Long id;
private String userName;
private int sexCode;
private String sexName;
private String note;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public int getSexCode() {
return sexCode;
}
public void setSexCode(int sexCode) {
this.sexCode = sexCode;
}
public String getSexName() {
return sexName;
}
public void setSexName(String sexName) {
this.sexName = sexName;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
}
新增一上Dao接口:com.xiaoxie.dao.UserDao
package com.xiaoxie.dao;
import com.xiaoxie.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Component;
import java.util.List;
@Mapper
@Component
public interface UserDao {
public int insertUser(User user);
public User getUser(Long id);
public List<User> getUserList(@Param("userName") String userName, @Param("note") String note, @Param("start") int start, @Param("limit") int limit);
public int updateUser(User user);
public int updateUserName(@Param("id") Long id, @Param("userName") String UserName);
public int deleteUser(Long id);
}
注意这里的dao接口使用了@Mapper来进行标注,同时使用了@Component,这个是为了在后面Service中做自动注入时不出现报错提示。
在resources/mybatis/mapper下新增一个mybatis的mapper映射文件:UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaoxie.dao.UserDao">
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id" parameterType="user">
insert into t_user(user_name,note,sex) VALUES (#{userName},#{note},#{sex.id})
</insert>
<select id="getUser" parameterType="long" resultType="user">
select id,user_name as userName,case sex when 1 then 'MALE' else 'FEMALE' end as sex,note from t_user where id=#{id}
</select>
<select id="getUserList" resultType="user">
select id,user_name as userName,case sex when 1 then 'MALE' else 'FEMALE' end as sex,note from t_user
<where>
<if test="userName != null">
and user_name = #{userName}
</if>
<if test="note != null">
and note = #{note}
</if>
</where>
limit #{start},#{limit}
</select>
<update id="updateUser" parameterType="user">
update t_user
<set>
<if test="userName != null">user_name=#{userName},</if>
<if test="note != null">note=#{note},</if>
<if test="sex != null">sex=#{sex.id}</if>
</set>
where id=#{id}
</update>
<update id="updateUserName">
update t_user
<set>
<if test="userName != null">user_name = #{userName}</if>
</set>
where id=#{id}
</update>
<delete id="deleteUser" parameterType="long">
delete from t_user where id = #{id}
</delete>
</mapper>
注意这个xml配置文件中namespace指定了具体的dao接口,同时在配置文件中使用user则代表User类(因为在User类添加过别名注解)
新增Spring Boot的启动类:com.xiaoxie.SpringBootStartApplication
package com.xiaoxie;
import org.apache.ibatis.annotations.Mapper;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan(basePackages = "com.xiaoxie.dao",annotationClass = Mapper.class)
public class SpringBootStartApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootStartApplication.class,args);
}
}
注意这里的@MapperScan注解,其中定义了,扫描的基础包以及对应有Mapper注解的类
新增service的的接口:com.xiaoxie.service.UserService
package com.xiaoxie.service;
import com.xiaoxie.pojo.User;
import java.util.List;
public interface UserService {
public User insertUser(User user);
public User getUser(Long id);
public List<User> getUserList(String userName, String note, int start, int limit);
public User updateUser(User user);
public User updateUserName(Long id,String userName);
public User deleteUser(Long id);
public User updateUserName2(Long id,String userName);
}
新增service的实现类:com.xiaoxie.service.impl.UserServiceImpl
package com.xiaoxie.service.impl;
import com.xiaoxie.dao.UserDao;
import com.xiaoxie.pojo.User;
import com.xiaoxie.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao = null;
@Transactional
@Override
public User insertUser(User user) {
userDao.insertUser(user);
return user;
}
@Override
public User getUser(Long id) {
return userDao.getUser(id);
}
@Override
public List<User> getUserList(String userName, String note, int start, int limit) {
return userDao.getUserList(userName,note,start,limit);
}
@Transactional
@Override
public User updateUser(User user) {
userDao.updateUser(user);
return user;
}
@Transactional
@Override
public User updateUserName( Long id, String userName) {
userDao.updateUserName(id,userName);
return userDao.getUser(id);
}
@Transactional
@Override
public User deleteUser(Long id){
//查用户
User user = userDao.getUser(id);
if(user == null)
return null;
int result = userDao.deleteUser(id);
if(result>0)
return user;
return null;
}
@Override
public User updateUserName2(Long id, String userName) {
userDao.updateUserName(id,userName);
return userDao.getUser(id);
}
}
新增controller类:com.xiaoxie.controller.UserController
package com.xiaoxie.controller;
import com.xiaoxie.enums.SexEnum;
import com.xiaoxie.pojo.User;
import com.xiaoxie.service.UserService;
import com.xiaoxie.vo.UserVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
@Controller
public class UserController {
@Autowired
private UserService userService = null;
//转换Vo到Po
private User ChangeToPo(UserVo userVo){
User user = new User();
user.setId(userVo.getId());
user.setUserName(userVo.getUserName());
user.setSex(SexEnum.getEnumById(userVo.getSexCode()));
user.setNote(userVo.getNote());
return user;
}
//转换Po到Vo
private UserVo changeToVo(User user){
UserVo userVo = new UserVo();
userVo.setId(user.getId());
userVo.setUserName(user.getUserName());
userVo.setSexCode(user.getSex().getId());
userVo.setSexName(user.getSex().getName());
userVo.setNote(user.getNote());
return userVo;
}
//po列表转为Vo列表
private List<UserVo> changeToVoes(List<User> poList){
List<UserVo> voList = new ArrayList<>();
for(User user:poList){
UserVo userVo = changeToVo(user);
voList.add(userVo);
}
return voList;
}
//JSP视图
@GetMapping("/restful")
public String page(){
return "restful";
}
//用户新增
@PostMapping("/insertUser")
@ResponseBody
public User insertUser(@RequestBody UserVo userVo){
User user = this.ChangeToPo(userVo);
return userService.insertUser(user);
}
@GetMapping(value = "/getUser/{id}")
@ResponseBody
public UserVo getUser(@PathVariable("id") Long id){
User user = userService.getUser(id);
return changeToVo(user);
}
@GetMapping("/getUserList/{userName}/{note}/{start}/{limit}")
@ResponseBody
public List<UserVo> getUserList(
@PathVariable("userName") String userName,
@PathVariable("note") String note,
@PathVariable("start") int start,
@PathVariable("limit") int limit
){
List<User> userList = userService.getUserList(userName,note,start,limit);
return changeToVoes(userList);
}
@PutMapping("/updateUser/{id}")
@ResponseBody
public User updateUser(@PathVariable("id") Long id,
@RequestBody UserVo userVo){
User user = this.ChangeToPo(userVo);
user.setId(id);
userService.updateUser(user);
return user;
}
@PatchMapping("/updateUserName/{id}/{userName}")
@ResponseBody
public User changeUserName(@PathVariable("id") Long id,@PathVariable("userName") String userName){
return userService.updateUserName(id,userName);
}
@DeleteMapping("/deleteUser/{id}")
@ResponseBody
public User deleteUser(@PathVariable("id") Long id){
return userService.deleteUser(id);
}
//对应到jsp视图
@GetMapping("/updateUserNamePage")
public String changUserNamePage(){
return "change_user_name";
}
//@PostMapping("/updateUserName2")
@PatchMapping("/updateUserName2") //如果使用这个注解则需要在jsp的表单form内部新增隐藏域:<input type="hidden" name="_method" id="_method" value="PATCH"/>
@ResponseBody
public User changeUserName2(Long id,String userName){
return userService.updateUserName2(id,userName);
}
}
从上面的controller类中我们可以看到有两个JSP页面是需要我们新增的,根据视图映射的配置,新增JSP的位置在webapp/WEB-INF/jsp目录下
在新增jsp页面前我们先把jquery的js文件复制到项目中
新增webapp/js目录并在这个目录下把jquery的js文件复制进来
新增jsp页面如下:
resetful.jsp
<%--
Created by IntelliJ IDEA.
User: adven
Date: 2021/7/13
Time: 23:16
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>REST测试页面</title>
<script src="../../js/jquery-3.2.1.min.js"></script>
<script type="text/javascript">
function post() {
var params = {
'userName':"李白",
'sexCode':1,
'note':"诗仙"
}
$.post({
url:"./insertUser",
//指定传递参数类型为json
contentType:"application/json",
//把JSON转化为字符串
data:JSON.stringify(params),
//成功后
success:function(result){
if(result == null || result.id == null){
alert("插入用户失败!");
return;
}
alert("插入用户成功。" + JSON.stringify(result));
}
});
}
function get() {
$.get("./getUser/28",function(user,status){
if(user==null || user==""){
alert("结果为空");
} else {
alert("用户信息为:" + JSON.stringify(user));
}
});
}
function getList() {
$.get("./getUserList/王五/来啊/0/3",function (userList,status) {
if(userList == null){
alert("结果为空");
} else {
alert("用户信息为:" + JSON.stringify(userList));
}
});
}
function updateUser() {
var params = {
'userName':'杜甫',
'sexCode':1,
'note':'诗圣'
}
$.ajax({
url:"./updateUser/30",
//请求类型
type:'PUT',
//传参类型
contentType:"application/json",
//把json转为字符串
data:JSON.stringify(params),
success:function(user,status){
if(user==null){
alert("结果为空");
} else {
alert("更新成功," + JSON.stringify(user));
}
}
});
}
function updateUserName() {
$.ajax({
url:"./updateUserName/100/赵六",
type:"PATCH",
success:function (user,status) {
if(user == null || user == ""){
alert("结果为空");
} else {
alert("更新成功!" + JSON.stringify(user))
}
}
});
}
function deleteUser() {
$.ajax({
url:"./deleteUser/12",
type:"DELETE",
success:function(user,status){
if(user==null || user==""){
alert("结果为空");
} else {
alert("删除用户成功!" + JSON.stringify(user));
}
}
});
}
</script>
</head>
<body>
<h2>测试RESTful请求</h2>
<button type="button" οnclick="post();">post</button><br/>
<button type="button" οnclick="get();">get</button><br/>
<button type="button" οnclick="getList();">getList</button><br/>
<button type="button" οnclick="updateUser();">updateUser</button><br/>
<button type="button" οnclick="updateUserName();">updateUserName</button><br/>
<button type="button" οnclick="deleteUser();">deleteUser</button>
</body>
</html>
change_user_name.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>表单进行用户名称修改</title>
</head>
<body>
<form id="form" action="./updateUserName2" method="post">
<table>
<tr>
<td>用户编号</td>
<td><input id="id" name="id"></td>
</tr>
<tr>
<td>用户名称</td>
<td><input id="userName" name="userName"></td>
</tr>
<tr>
<td></td>
<td align="right">
<input id="submit" name="submit" type="submit">
</td>
</tr>
</table>
<input type="hidden" name="_method" id="_method" value="PATCH"/>
</form>
</body>
</html>
启动Spring Boot则可以根据controller中定义的请求路径与请求URI进行对应来进行测试了。
关于@RestController注解说明
前后端的分离,使得使用JSON作为前后端交互已经非常普遍了,向前面的项目中如何每个方法上加上@ResponseBody才能把数据模型转为JSON,显得会有些冗余,这个时候Spring MVC在支持REST风格中提供了@RestController,这个注解放在controller类上,它可以把控制器返回的对象转为JSON数据集。
新增一个工具类:com.xiaoxie.utils.ChangePoAndVo
package com.xiaoxie.utils;
import com.xiaoxie.enums.SexEnum;
import com.xiaoxie.pojo.User;
import com.xiaoxie.vo.UserVo;
import java.util.ArrayList;
import java.util.List;
public class ChangePoAndVo {
//转换Vo到Po
public static User ChangeToPo(UserVo userVo){
User user = new User();
user.setId(userVo.getId());
user.setUserName(userVo.getUserName());
user.setSex(SexEnum.getEnumById(userVo.getSexCode()));
user.setNote(userVo.getNote());
return user;
}
//转换Po到Vo
public static UserVo changeToVo(User user){
UserVo userVo = new UserVo();
userVo.setId(user.getId());
userVo.setUserName(user.getUserName());
userVo.setSexCode(user.getSex().getId());
userVo.setSexName(user.getSex().getName());
userVo.setNote(user.getNote());
return userVo;
}
//po列表转为Vo列表
public static List<UserVo> changeToVoes(List<User> poList){
List<UserVo> voList = new ArrayList<>();
for(User user:poList){
UserVo userVo = changeToVo(user);
voList.add(userVo);
}
return voList;
}
}
新增一个controller类:com.xiaoxie.controller.UserController2
package com.xiaoxie.controller;
import com.xiaoxie.pojo.User;
import com.xiaoxie.service.UserService;
import com.xiaoxie.utils.ChangePoAndVo;
import com.xiaoxie.vo.UserVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
@RestController //方法默认使用JSON返回
public class UserController2 {
@Autowired
private UserService userService = null;
//映射JSP视图
@GetMapping(value = "/restful2")
public ModelAndView index(){
ModelAndView mv = new ModelAndView("restful");
return mv;
}
//获取用户
@GetMapping(value = "/getUser2/{id}")
public UserVo getUser(@PathVariable("id") Long id){
User user = userService.getUser(id);
return ChangePoAndVo.changeToVo(user);
}
}
在这个类上使用了注解:@RestController,则下面的方法默认是返回JSON数据。
如果我们需要映射JSP页面上去则需要使用ModelAndView指定视图位置。
关于视图的渲染
在Spring MVC中,IoC容器启动时注册了两个HttpMessageConverter接口的实现类,分别如下:
StringHttpMessageConverter
MappingJackson2HttpMessageConverter
在HttpMessageConverter接口中定了了canWrite方法,方法如下
boolean canWrite(Class<?> clazz,MediaType mediaType);
这个方法中MediaType可以传入的媒体类型,Spring MVC在执行控制器的方法后回去遍历注册的HttpMessageConverter接口的实现类使用canWrite方法去判断是否拦截控制器的返回。
在@RequestMapping、@GetMapping等注解中还存在consumes和produces两个属性
consumes:限定方法接收什么类型的请求体
produces:限定返回的媒体类型,只有request请求头中Accept中包含的类型才返回
在UserController2类中新增方法
//获取用户名称
@GetMapping(value = "/getUserName/{id}",
//设置可以接受任意类型的请求体
consumes = MediaType.ALL_VALUE,
//限定返回的是文本类型
produces = MediaType.TEXT_PLAIN_VALUE
)
public String getUserName(@PathVariable("id") Long id){
User user = userService.getUser(id);
//返回字符串名称
return user.getUserName();
}
上面这个方法返回的结果会是一个文本
控制器使用了@RestController注解,则默认会使用JSON作为返回类型,它会默认方法标注为“application/json;charset=UTF-8”,这样的话当控制器方法结束后,Spring会遍历注册好HttpMessageConverter接口的实现类,这时MappingJackson2HttpMessageConverter的canWrite方法返回true,则会启用这个实现类转为JSON数据集类型。然而上面的方法中则是修改了原有@ResetController默认的JSON类型,会被Spring MVC注册好的StringHttpMessageConverter拦截,这样这就会转为一个字符串。
当HttpMessageConverter机制没有处理的类型,它会流转到ViewResolver。在Spring对REST风格的支持中,还会提供协商视图解析器(ContentNegotiatingViewResolver),当控制器返回结果找不到HttpMessageConver解析时,主会流转到这里进行解析。如,返回的是ModelAndView,它会去处理这个ModelAndView,先去解析View的类型,然后根据返回,找到最好的视图解析器去处理。
Spring MVC内置好了如下几个视图解析器
BeanNameViewResolver:根据请求URI名称找到对应的视图
ViewResolverComposite:视图解析器组合
InternalResourceViewResolver:逻辑视图解析器(最为常用,JstlViewResolver是它的子类)
对HTTP状态码、异常和响应头处理
Spring提供了实体封装类ResponseEntity和注解@ResponseStatus
ResponseEntity:可以有效封装错误消息和状态码
@ResponseStatus可以配置指定的响应码给客户
通常来说,后台请求成功后会返回一个状态码200,但是要注意这个状态只能表示请求成功,并不能完全表控制器对资源的操作成功。
在控制器类UserController2中新增两个方法
@PostMapping(value = "/insertUser/entity")
public ResponseEntity<UserVo> insertUser(@RequestBody UserVo userVo){
User user = ChangePoAndVo.ChangeToPo(userVo);
userService.insertUser(user);
UserVo userVo1 = ChangePoAndVo.changeToVo(user);
HttpHeaders headers = new HttpHeaders();
String success = (userVo1 == null || userVo1.getId() == null)?"false":"true";
//设置响应头
headers.add("success",success);
//返回创建成功的状态码
return new ResponseEntity<UserVo>(userVo1,headers, HttpStatus.CREATED);
}
@PostMapping(value = "/insertUser/annotation")
@ResponseStatus(HttpStatus.CREATED) //指定资源创建成功 201
public UserVo insertUserAnnotation(@RequestBody UserVo userVo){
User user = ChangePoAndVo.ChangeToPo(userVo);
userService.insertUser(user);
UserVo userVo1 = ChangePoAndVo.changeToVo(user);
return userVo1;
}
在这两个方法中,第一个使用了ResponseEntity,一个使用了@ResponseStatus注解
在使用ResponseEntity的方法中可以设置对应的响应头,最后把响应的数据、响应头、响应状态一起返回
在使用@ResponseStatus注解的方法中,只是直接在注解中设置响应状态
修改restful.jsp页面,新增js函数:postStatus()
<%--
Created by IntelliJ IDEA.
User: adven
Date: 2021/7/13
Time: 23:16
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>REST测试页面</title>
<script src="../../js/jquery-3.2.1.min.js"></script>
<script type="text/javascript">
function post() {
var params = {
'userName':"李白",
'sexCode':1,
'note':"诗仙"
}
$.post({
url:"./insertUser",
//指定传递参数类型为json
contentType:"application/json",
//把JSON转化为字符串
data:JSON.stringify(params),
//成功后
success:function(result){
if(result == null || result.id == null){
alert("插入用户失败!");
return;
}
alert("插入用户成功。" + JSON.stringify(result));
}
});
}
function get() {
$.get("./getUser/28",function(user,status){
if(user==null || user==""){
alert("结果为空");
} else {
alert("用户信息为:" + JSON.stringify(user));
}
});
}
function getList() {
$.get("./getUserList/王五/来啊/0/3",function (userList,status) {
if(userList == null){
alert("结果为空");
} else {
alert("用户信息为:" + JSON.stringify(userList));
}
});
}
function updateUser() {
var params = {
'userName':'杜甫',
'sexCode':1,
'note':'诗圣'
}
$.ajax({
url:"./updateUser/30",
//请求类型
type:'PUT',
//传参类型
contentType:"application/json",
//把json转为字符串
data:JSON.stringify(params),
success:function(user,status){
if(user==null){
alert("结果为空");
} else {
alert("更新成功," + JSON.stringify(user));
}
}
});
}
function updateUserName() {
$.ajax({
url:"./updateUserName/100/赵六",
type:"PATCH",
success:function (user,status) {
if(user == null || user == ""){
alert("结果为空");
} else {
alert("更新成功!" + JSON.stringify(user))
}
}
});
}
function deleteUser() {
$.ajax({
url:"./deleteUser/12",
type:"DELETE",
success:function(user,status){
if(user==null || user==""){
alert("结果为空");
} else {
alert("删除用户成功!" + JSON.stringify(user));
}
}
});
}
function postStatus() {
//请求体
var params={
'userName':'诸葛亮',
'sexCode':1,
'note':'诸葛孔明'
};
//var url = "./insertUser/entity";
var url = "./insertUser/annotation";
$.post({
url:url,
//传递参数的类型JSON
contentType:"application/json",
//json转字符串
data:JSON.stringify(params),
//成功后
success:function(result,status,jqXHR){
//获取响应头
var success = jqXHR.getResponseHeader("success");
//获取状态码
var status = jqXHR.status;
if(success != null){
alert("响应头参数是:" + success + "状态码是:" + status);
}
alert("响应状态码:" + status);
if(result == null || result.id == null){
alert("插入失败");
return ;
}
alert("插入成功");
}
});
}
</script>
</head>
<body>
<h2>测试RESTful请求</h2>
<button type="button" οnclick="post();">post</button><br/>
<button type="button" οnclick="get();">get</button><br/>
<button type="button" οnclick="getList();">getList</button><br/>
<button type="button" οnclick="updateUser();">updateUser</button><br/>
<button type="button" οnclick="updateUserName();">updateUserName</button><br/>
<button type="button" οnclick="deleteUser();">deleteUser</button><br/>
<button type="button" οnclick="postStatus();">postStatus</button>
</body>
</html>
异常处理
自定义一个异常com.xiaoxie.exception.MyUserException
package com.xiaoxie.exception;
/**自定义异常类*/
public class MyUserException extends RuntimeException {
//异常编码
private Long code;
//自定义异常信息
private String customMsg;
public MyUserException(){}
public MyUserException(Long code,String customMsg){
super();
this.code = code;
this.customMsg = customMsg;
}
public Long getCode() {
return code;
}
public void setCode(Long code) {
this.code = code;
}
public String getCustomMsg() {
return customMsg;
}
public void setCustomMsg(String customMsg) {
this.customMsg = customMsg;
}
}
定义一个控制器通知com.xiaoxie.advice.UserControllerAdvice
package com.xiaoxie.advice;
import com.xiaoxie.exception.MyUserException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/** userController的控制器通知 */
@ControllerAdvice(
//指定拦截的包
basePackages = {"com.xiaoxie.controller.*"},
//限制做了对应注解的才会被拦截
annotations = {Controller.class, RestController.class}
)
public class UserControllerAdvice {
//定义拦截的异常类型
@ExceptionHandler(value = MyUserException.class)
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) //定义服务器错误状态码
public Map<String,Object> exception(MyUserException ex){
Map<String,Object> map = new HashMap<>();
//组装异常信息
map.put("code",ex.getCode());
map.put("message",ex.getCustomMsg());
return map;
}
}
这个通知器使用注解@ControllerAdvice,它其中属性basePackages指定了拦截的controller所在基础包,annotations限定做了控制器使用了@Controller、@RestController注解的控制器才会拦截(这里了可以自定义一个注解,把这个注解应用到要被拦截的控制器上)
这个类中的方法exception中,使用了如下注解
@ExceptionHandler:表示对异常拦截后做处理,其中value属性指定了对那个异常做拦截,这里指定了自定义的异常类MyUserException.class
在控制器UserController2中新增方法
@GetMapping(value = "getUserExp/{id}")
@ResponseStatus(HttpStatus.OK) //响应成功
public UserVo getUserExp(@PathVariable Long id){
User user = userService.getUser(id);
if(user == null){ //如果没有找到用户则抛出指定异常
throw new MyUserException(1L,"找不到用户【" + id + "】的信息" );
}
UserVo userVo = ChangePoAndVo.changeToVo(user);
return userVo;
}
这个方法中可以看到,当查询用户没有查询到时会抛出一个MyUserException的异常
客户端请求RestTemplate
RestTemplate是一个模板类,用这来请求后端暴露的URI以获取资源,它的底层是通过类HttpURLConnectioni实现的。
使用RestTemplate进行HTTP GET请求
新增一个类进行RestTemplate请求测试:com.xiaoxie.restTemplate.RestTemplate4User
package com.xiaoxie.restTemplate;
import com.xiaoxie.vo.UserVo;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class RestTemplate4User {
public static RestTemplate restTemplate = new RestTemplate();
/**根据id获取用户*/
public static UserVo getUser(Long id){
//消费服务,三个参数:1.请求url;2.返回类型;3.URI路径参数
UserVo userVo = restTemplate.getForObject("http://127.0.0.1:8080/getUser/{id}" ,
UserVo.class, id);
//打印用户名称
System.out.println("获取到的用户名称:" + userVo.getUserName());
return userVo;
}
}
上面的getUser方法中调用了RestTemplate的getForObject方法,这个方法有三个参数
第一个参数URI,表示请求的资源URI
第二个参数UserVo.class,表示请求会返回的类型是UserVo,因为服务器返回的是JSON数据,RestTemplate会根据这个参数在内部把JSON转为UserVo对象
第三个参数,对应为URI中对应的参数,这是一个可变长的参数,当URI中有多个参数时,这里按序顺写上即可
在Spring Boot启动类后面添加如下代码:
System.out.println("****************** 使用RestTemplate请求后端资源 *********************");
UserVo userVo = RestTemplate4User.getUser(30L);
在Spring Boot启动完成后,在控制台可以看到相应的输出
如果存在多个参数的时候,可以把参数使用Map集合进行封装,Map集合中的key要与对应的请求参数名称一一对应。
在RestTemplate4User类中新增一个方法
/**封装多个参数获取用户*/
public static List<UserVo> findUsers(String userName,String note,int start,int limit){
//封装参数为Map,其中的key需要与请求uri中参数一一对应
Map<String,Object> params = new HashMap<>();
params.put("userName",userName);
params.put("note",note);
params.put("start",start);
params.put("limit",limit);
String url ="http://127.0.0.1:8080/getUserList/{userName}/{note}/{start}/{limit}";
//请求后端资源
ResponseEntity<List> responseEntity = restTemplate.getForEntity(url, List.class, params);
List<UserVo> userVos = responseEntity.getBody();
return userVos;
}
这里调用的是RestTemplate的getForEntity方法。
POST请求传递JSON请求体
在RestTemplate4User类中新增一个方法
public static User insertUser(UserVo userVo){
//请求头
HttpHeaders headers = new HttpHeaders();
//设置请求内容为JSON类型
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
//创建请求的实体对象
HttpEntity<UserVo> requestEntity = new HttpEntity<>(userVo,headers);
//向后端请求资源时传递请求实体对象,并且会回填ID
User user = restTemplate.postForObject("http://127.0.0.1:8080/insertUser", requestEntity, User.class);
System.out.println("创建用户成功!新用户ID:" + user.getId());
return user;
}
先定义一个请求头,在请求头中定定请求体内容为JSON类型,然后把它与请求实体(UserVo对象)绑定到请求实体上,然后restTemplate调用postForObject方法把请求实体对像传递过去。
在ResetTemplate4User中新增一个方法
public static void deleteUser(Long id){
//先去查询是否存在用户
UserVo user = getUser(id);
if(user!=null) {
restTemplate.delete("http://127.0.0.1:8080/deleteUser/{id}", id);
System.out.println("删除用户id为:" + id + "的用户!");
} else {
System.out.println("要删除的用户不存在!");
}
}
这个删除的方法有一点要注意的就是,RestTemplate的delete方法返回值是void,方法的参数中也不需要指定返回对象的类型。
获取响应头、状态码和资源交换
当我们对资源请求的时候如果不成功,这些信息可以存放在响应头中,服务器也会返回错误的状态码。
在RestTemplate4User中新增方法
public static User insertUserEntity(UserVo userVo){
//请求头
HttpHeaders headers = new HttpHeaders();
//请求类型
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
//绑定请求体和头
HttpEntity<UserVo> request = new HttpEntity<>(userVo,headers);
//请求服务器
ResponseEntity<User> userResponseEntity = restTemplate.postForEntity(
"http://localhost:8080/insertUser/entity", request, User.class);
//获取响应体
User user = userResponseEntity.getBody();
//获取响应头
HttpHeaders respHeaders = userResponseEntity.getHeaders();
//获取响应属性
List<String> success = respHeaders.get("success");
//响应状态码
int status = userResponseEntity.getStatusCodeValue();
System.out.println("响应状态码:" + status);
System.out.println("响应头success:" + success.toString());
System.out.println("新增用户的ID:" + user.getId());
return user;
}
这里使用了RestTemplate的postForEntity方法,这个方法会返回一个ResponseEntity对象,这个对象就包含了返回响应体、状态码、响应头信息。
RestTemplate中的exchange方法实例
在RestTemplate4User中新增方法
public static User userExchange(UserVo userVo,Long id){
//请求头
HttpHeaders headers = new HttpHeaders();
//请求类型
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
//绑定请求头与请求体
HttpEntity<UserVo> request = new HttpEntity<>(userVo,headers);
String url = "http://localhost:8080/insertUser/entity";
//请求服务器
ResponseEntity<User> userEntity = restTemplate.exchange(url, HttpMethod.POST, request, User.class);
//获取响应体
User user = userEntity.getBody();
//获取响应头
HttpHeaders respHeaders = userEntity.getHeaders();
//响应头属性
List<String> success =respHeaders.get("success");
//响应码
int status = userEntity.getStatusCodeValue();
System.out.println("响应状态码:" + status);
System.out.println("响应头success:" + success.toString());
System.out.println("新增用户的ID:" + user.getId());
//修改URL地址
url = "http://localhost:8080/getUser/{id}";
ResponseEntity<UserVo> userVoEntity = restTemplate.exchange(url, HttpMethod.GET, null, UserVo.class, id);
//获取响应体
UserVo u = userVoEntity.getBody();
System.out.println("获取到的用户名称:" + u.getUserName());
return user;
}
在exchange方法中可以指定请求头、请求体、HTTP请求类型及参数……