文章目录
- RestAPI
- Content-Type
- Servlet
- HttpServletRequest
- 请求行
- 请求头
- 请求参数
- HttpServletResponse
- 状态码
- SpringBoot中支持
- Controller与Method
- RequestMapping
- ExceptionHandler
- 统一异常捕捉
- 参数自动验证
- API接口示例
- Form方式请求
- 文件上传
RestAPI
RESTful是目前非常流行的一种互联网软件架构,其核心概念是资源(网络中任何东西都看作资源)。通过URL定位资源,用HTTP请求方式表示操作:
- URL:要什么(资源)?
- HTTP Method:干什么(动作)?
- Status Code:结果怎样?
Content-Type
MediaType,即是Internet Media Type,互联网媒体类型;也叫做MIME类型,在Http协议消息头中,使用Content-Type来表示具体请求中的媒体类型信息。常见的媒体格式类型如下:
- text/html : HTML格式
- text/plain :纯文本格式
- text/xml : XML格式
- image/gif :gif图片格式
- image/jpeg :jpg图片格式
- image/png:png图片格式
- application/xhtml+xml :XHTML格式
- application/xml : XML数据格式
- application/atom+xml :Atom XML聚合格式
- application/json : JSON数据格式
- application/pdf :pdf格式
- application/msword : Word文档格式
- application/octet-stream : 二进制流数据(如常见的文件下载)
- application/x-www-form-urlencoded : form表单数据(会被编码为key/value格式发送到服务器)
- multipart/form-data : 需要在表单中进行文件上传时,就需要使用该格式
Servlet
Servlet 是运行在 Web 服务器或应用服务器上的程序,它是’Web 浏览器或其他 HTTP 客户端的请求‘和‘ HTTP 服务器上’的数据库或应用程序之间的中间层。
在接收到客户端请求时,Servlet容器会为请求创建代表HTTP请求的ServletRequest对象,和对此请求进行响应的ServletResponse对象,并将这两个对象传给service方法,由service方法来处理请求。service方法会根据请求类型来做转发,调度不同的方法来处理请求。
HttpServletRequest
HttpServletRequest中包含了客户端HTTP请求的所有信息,主要为三部分信息:请求行、请求头、请求正文。
请求行
请求行中包含:请求方法、资源名、路径、Http版本等信息。HttpServletRequest中实现了一揽子的方法,以便简单快捷的操作获取:
方法声明 | 功能说明 |
String getMethod() | 获取请求方式:如GET、POST等 |
String getRequestURI() | 获取资源名称部分:即位于URL的主机与端口之后,参数部分之前部分 |
String getQueryString() | 获取参数部分:即URL中问号(?)以后的内容 |
String getProtocol() | 获取协议名和版本:如HTTP/1.0或HTTP/1.1等 |
String getContextPath() | 获取URL中属于WEB应用程序的路径(以 |
String getServletPath() | 获取Servlet的名称或所映射的目录 |
String getRemoteAddr() | 获取远端(客户端)的IP地址 |
String getRemoteHost() | 获取客户端的完整主机名;若无法解析出,则返回其IP地址 |
int getRemotePort() | 客户端连接的端口号 |
String getLocalAddr() | 获取WEB服务器上接收当前请求的IP地址 |
String getLocalName() | 获取WEB服务器上接收当前请求的主机名 |
int getLocalPort() | 获取WEB服务器上接收当前请求的端口号 |
String getServerName() | 获取当前请求所指向的主机名:即HTTP请求消息中Host头字段所对应的主机名部分 |
int getServerPort() | 获取当前请求所所连接的服务器端口号:即HTTP请求消息中Host头字段所对应的端口号部分 |
String getScheme() | 获取请求协议名:如http、https、ftp等 |
StringBuffer getRequestURL() | 获取客户端发起请求时的完整URL,包括协议、服务器名、端口号、资源路径等,但不包括后面查询参数部分 |
请求头
通过请求头可向服务器传递附加信息,如:可接受的数据类型、是否保持TCP连接等。Servlet中也提供了一系列操作方法:
方法声明 | 功能说明 |
String getHeader(String name) | 获取指定头的值:若头中未包含 |
Enumeration getHeaders(String name) | 返回指定头字段的可枚举集合对象 |
Enumeration getHeaderNames() | 返回所有请求头字段名的可枚举集合对象 |
int getIntHeader(String name) | 返回指定头字段对应int值:若字段不存在,返回-1;若字段不是int类型的,抛出NumberFormatExcption |
long getDateHeader(String name) | 返回指定头字段对应GMT时间戳(毫秒级) |
String getContentType() | 获取Content-Type字段值 |
int getContentLength() | 获取Content-Length头字段的值 |
String getCharacterEncoding() | 获取消息实体部分的字符集编码 |
Cookie[] getCookies() | 获取所有客户端传递过来的Cookie对象,若没有返回null |
请求参数
请求参数中包含用户提交的数据,是请求中最重要的部分。Servlet中也提供了一系列操作方法:
方法声明 | 功能说明 |
String getParameter(String name) | 获取指定名称的参数值:若没有,返回null;若有参数名,但没有参数值,返回空字符串;若有多个,返回第一个 |
String[] getParameterValues(String name) | 返回指定参数对应的所有值 |
Enumeration getParameterNames() | 返回请求消息中所有参数名的可枚举对象 |
Map getParameterMap() | 返回参数的Map对象(键值对) |
HttpServletResponse
HttpServletResponse用于完成Http的响应。
状态码
默认情况下会返回如下状态码:
- 200:程序正常执行(完整处理了Http请求)时返回的错误码;
- 404:url路径错误时;
- 500:程序执行过程中直接抛出异常时,返回此错误码;
也可通过函数设定:
方法声明 | 功能说明 |
void setStatus(int sc) | 设定响应状态码 |
void sendError(int sc) | 使用指定的状态码向客户端返回一个错误响应,并清空缓冲区 |
void sendError(int sc, Sting msg) | 设定状态码与错误信息,并清空缓冲区 |
SpringBoot中支持
Spring Boot全面支持开发RESTful程序,通过不同的注解来支持前端的请求。
Controller与Method
Spring中通过注解来定义控制器与API接口:
- @Controller:响应页面,表示当前的类为控制器;
- @RestController:是@ResponseBody和@Controller的结合,表明当前类是控制器且返回的是一组数据(ResponseBody)而非页面;
- @RequestMapping:告诉控制器URL映射的映射方式,以下是
@RequestMapping(method = RequestMethod.XXX)的简写
:
- @GetMapping:对应查询,表明是一个查询URL映射;
- @PostMapping:对应增加,表明是一个增加URL映射;
- @PutMapping:对应更新,表明是一个更新URL映射;
- @DeleteMapping:对应删除,表明是一个删除URL映射;
@RestController
@RequestMapping("test") // 路径
public class TestController {
// ...
}
RequestMapping
RequestMapping 用于映射请求。用于类级别时,将特定请求(路径)映射此类控制器上;用于方法时,会进一步控制映射关系:
- value: 指定请求的实际地址, 比如 /action/info;
- method: 指定请求的method类型, GET、POST、PUT、DELETE等;
- consumes: 指定处理请求的提交内容类型(Content-Type),例如application/json, text/html;
- produces: 指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回;
- params: 指定request中必须包含某些参数值是,才让该方法处理;
- headers: 指定request中必须包含某些指定的header值,才能让该方法处理请求;
其中,consumes, produces使用content-typ信息进行过滤信息;headers中可以使用content-type进行过滤和判断。
可通过value映射到多个路径(可通过../mult
, ../one
, ../two
来访问):
@GetMapping(value = {"multi", "one", "two"})
public String multiMap(HttpServletRequest request) {
return request.getRequestURI();
}
ExceptionHandler
通过ExceptionHandler可以统一处理异常,先定义统一的异常类:
public class WebTestRuntimeException extends RuntimeException {
private static final long serialVersionUID = 1L;
private int code;
private Exception inner;
public WebTestRuntimeException(int errorCode, String errorMsg) {
super(errorMsg);
this.code = errorCode;
}
// ...
}
以及统一的应答类:
public class BasicResponse implements Serializable {
private static final long serialVersionUID = 1L;
private int error;
private String errorMsg;
private Object result = null;
// ...
}
统一异常捕捉
定义如下类统一处理异常:
@Slf4j
@ControllerAdvice
public class ControllerExceptionHandler {
@ResponseBody
@ExceptionHandler
public BasicResponse WebExceptionHandler(Exception ex) throws NoSuchFieldException {
BindingResult bindingResult = null;
if (ex instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException exArg = (MethodArgumentNotValidException) ex;
bindingResult = exArg.getBindingResult();
} else if (ex instanceof BindException) {
bindingResult = ((BindException) ex).getBindingResult();
}
try {
if (bindingResult != null && bindingResult.hasErrors()) {
log.error("WebTest全局异常处理信息:{}", ex.getMessage());
String fieldError = bindingResult.getFieldError().getDefaultMessage();
return buildInvalidResult(fieldError);
}
if (ex instanceof ConstraintViolationException) {
log.error("WebTest全局异常处理信息:{}", ex.getMessage());
ConstraintViolationException e = (ConstraintViolationException) ex;
String fieldError = e.getConstraintViolations().stream().findFirst().get().getMessage();
return buildInvalidResult(fieldError);
}
} catch (Exception e) {
log.error("process argumentsError throws error", e);
return new BasicResponse(ErrorCodeEnum.ERROR_Invalid_Argument.getCode(), ex.getMessage());
}
if (ex instanceof WebTestRuntimeException) {
log.error("WebTest全局异常处理信息:{}", ex.getMessage());
WebTestRuntimeException hnException = (WebTestRuntimeException) ex;
return new BasicResponse(hnException.getCode(), hnException.getMessage());
}
log.error("WebTest全局异常处理信息", ex);
return new BasicResponse(ErrorCodeEnum.ERROR_FAIL.getCode(), ex.getMessage());
}
private BasicResponse buildInvalidResult(String field) {
String[] fieldError = field.split("=",2);
Integer strCode;
String strMsg;
if(fieldError.length>1){
strCode =Integer.valueOf(fieldError[0].trim());
strMsg = fieldError[1];
}
else{
strCode = ErrorCodeEnum.ERROR_Invalid_Argument.getCode();
strMsg = fieldError[0];
}
return new BasicResponse(strCode, strMsg.trim());
}
}
buildInvalidResult是根据参数上@Validated验证失败(抛出ConstraintViolationException等异常)构造错误异常返回。
参数自动验证
Spring验证框架提供了入参检验注解:
为了使用Validated自动验证,定义时需要增加注解(@Pattern注解允许对应变量为空,只有非空时才做验证):
@Data
@ApiModel
public class AddUserRequest implements Serializable {
private static final long serialVersionUID = 1L;
@NotBlank(message = "100001= idName不能为空")
private String idName;
@Pattern(regexp = "[0-3]", message = "100001= type错误,只能(0-3)的数字")
private String type; // 可以为空;非空只能是0~3的数字
@Pattern(regexp = "|\\d{7,11}", message = "100001= phone格式不正确(11位数字)")
private String phone; // 可以为空;非空只只能是7~11位数字
@NotNull(message = "100001= isSelf不能为空")
@Range(min = 0, max = 10, message = "100001= count必须是0~10")
private Integer count; // Range注解不能处理null,需要增加NotNull限制
}
为了使前面的注解生效,需要在参数上添加@Validated注解:
@PostMapping(value = "addUser")
public BasicResponse addUser(@Validated @RequestBody AddUserRequest reqBody) {
// ...
}
当传入的参数验证失败时,会抛出ConstraintViolationException异常,然后就会由前面的ControllerExceptionHandler捕捉。
API接口示例
RestAPI接口统一放在Controller类中:
@RestController
@RequestMapping("test")
public class TestController {
@GetMapping("paramGet") // 请求 ../test/paramGet?msg=12
public String paramGet(HttpServletRequest request, HttpServletResponse resp, String msg) throws IOException {
resp.setStatus(2XX); // 设定返回状态码,默认200
return msg;
}
@PutMapping(value = "paramPut") // 请求 ../test/paramPut?msg=12&others=34
public String paramPut(@RequestParam String msg, @RequestParam String others) {
// ...
}
@PutMapping("testPut") // data以body方式传递
public void testPut(@RequestBody TestData data) {
// ...
}
@DeleteMapping(value = "testDelete") // 请求 ../test/testDelete?id=12
public String testDelete(@RequestParam String id) {
// ...
}
@PostMapping("testPost")
public void testPost(@RequestBody TestData data) {
// 自动解析为类(需要Json格式匹配)
}
@PostMapping(value = "postMsg", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public String postMsg(@RequestBody String msg) {
// 所有body以字符串方式获取,然后自己解析
}
@GetMapping("/msg/{key}") // 请求 ../test/12
public String pathMsg(@PathVariable("key") String key) {
// 从路径上获取值,key为12
}
}
Form方式请求
form是一种常用的请求方式,其参数是作为body方式传递(POST中body默认是Json字符串;GET中参数是放在URL上):
// curl -X POST ".../test/formMsg" -H "accept: */*" -H "Content-Type: application/x-www-form-urlencoded" -d "msg=123&others=234"
@PostMapping(value = "formMsg", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public String formMsg(@RequestParam String msg, @RequestParam String others) {
// ...
}
文件上传
通过MultipartFile可以方便地上传文件:
@PostMapping(value = "/uploadMulti", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String uploadMulti(@RequestParam("file") MultipartFile[] files) {
for (MultipartFile partFile : files) {
_logger.info("File: {}, Ori: {}, Type: {}, Size: {}",
partFile.getName(), partFile.getOriginalFilename(),
partFile.getContentType(), partFile.getSize());
StringBuilder sb = new StringBuilder("\n");
try (BufferedReader reader = new BufferedReader(new InputStreamReader(partFile.getInputStream()))) {
String line = reader.readLine();
while (line != null) {
sb.append(line);
sb.append("\n");
line = reader.readLine();
}
_logger.info("Contents: {}", sb.toString());
} catch (Exception ex) {
ex.printStackTrace();
}
}
return "Files: " + Arrays.stream(files).map(MultipartFile::getOriginalFilename).collect(Collectors.joining("; "));
}
// curl -X POST ".../test/uploadFile" -H "accept: */*" -H "Content-Type: multipart/form-data" -F "file=@Python-learning.md;type="
@PostMapping(value = "/uploadFile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String uploadFile(@RequestParam("file") MultipartFile file) {
_logger.info("File: {}, Ori: {}, Type: {}, Size: {}",
file.getName(), file.getOriginalFilename(),
file.getContentType(), file.getSize());
StringBuilder sb = new StringBuilder("\n");
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) {
String line = reader.readLine();
while (line != null) {
sb.append(line);
sb.append("\n");
line = reader.readLine();
}
_logger.info("Contents: {}", sb.toString());
} catch (Exception ex) {
ex.printStackTrace();
}
return String.format("received: %s, size: %d, type: %s", file.getOriginalFilename(),
file.getSize(), file.getContentType());
}
swagger中无法处理上传多个文件,通过OkHttp可以方便地实现: