- 学习内容
- 什么Restful
- Restful API及其测试用例的编写
- MockMVC详解
- @JsonView
- 请求表单与请求JSON的区别
- 关于日期格式
- 请求参数校验
- BindingResult
- 自定义校验注解
- SpringBoot的默认异常处理与控制器增强
- 自定义校验异常
- 手动控制返回状态
- 服务拦截
- Swagger
- WireMock
- 异步处理
- 完整代码
本章主要开发一些REST风格的服务接口,后面章节中的认证授权模块会为这些服务接口提供安全保护,在这一章中,你会学习开发REST风格服务接口时,一些常用的技巧和工具。我们会介绍如何拦截服务接口来提供一些通用的功能(例如记日志),还会介绍如何通过多线程来提高服务的性能,以及如何自动生成服务文档和伪造服务等。...
学习内容
- 使用Spring MVC编写Restful API
- 使用Spring MVC处理web应用常见的需求:静态资源、异常、拦截器、文件上传下载、异步请求
- Restful API开发常用辅助框架:Swagger-生成文档;WireMock-伪造服务
本章内容是后续章节的基础,Spring Security保护的就是本应用
什么Restful
简单而言,就是:
- 使用url表示资源
- http方法表示对资源的行为
- http状态码表示不同的结果
- 使用JSON交互数据
如:
输入图片说明Restful只是一种开发风格,不是强制标准
Restful API及其测试用例的编写
完整代码:02-imooc-security-restful-test
不管是开发什么服务,一般遵循测试先行的原则,即先写测试代码
测试用例
伪造Servlet环境
package com.imooc.web.controller;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @Author 张柳宁
* @Description
* @Date Create in 2018/4/3
* @Modified By:
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@WebAppConfiguration("src/main/resources")
public class UserControllerTest {
private MockMvc mockMvc;
@Autowired
private WebApplicationContext wc;
@Before
public void setUp() throws Exception {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wc).build();
}
/**
* 测试 query 方法
*
* @throws Exception
*/
@Test
public void whenQuerySuccess() throws Exception {
//perform - 发送请求
// get - 模拟get请求
// contentType - 设置期望的返回数据格式
// param - 设置请求参数,表单参数
// andExpect - 设置期望的返回
// status().isOk() - 服务结果为成功
// andExpect(jsonPath("$.length()").value(3)) - 返回是一个json集合,长度为3;jsonPath表达式可以在github中查询jsonPath项目
// .andReturn().getResponse().getContentAsString() - 控制台打印结果
String result = mockMvc.perform(
get("/user")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.param("username", "jojo")
.param("age", "18")
.param("ageTo", "60")
.param("xxx", "yyy")
.param("size", "15")
.param("page", "3")
.param("sort", "age,desc")
).andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(3))
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}
}
在andExpect
中编写期望;一般先判断状态,再判断内容
控制器
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping
public List<User> query(UserQueryCondition condition,
@PageableDefault(page = 2, size = 17, sort = "username,asc") Pageable pageable){
//反射打印pojo成员变量
System.out.println(ReflectionToStringBuilder.toString(condition, ToStringStyle.MULTI_LINE_STYLE));
System.out.println(pageable.getPageSize());
System.out.println(pageable.getPageNumber());
System.out.println(pageable.getSort());
List<User> users = new ArrayList<>();
users.add(new User());
users.add(new User());
users.add(new User());
return users;
}
}
@PageableDefault
用于设置默认分页与排序
测试用例执行结果
com.imooc.dto.UserQueryCondition@59a09be[
username=jojo
age=18
ageTo=60
xxx=yyy
]
15
3
age: DESC
[{"username":null,"password":null},{"username":null,"password":null},{"username":null,"password":null}]
ReflectionToStringBuilder
是common-lang
提供的
MockMVC详解
发送表单请求
@Test
public void whenQuerySuccess() throws Exception {
String result = mockMvc.perform(
get("/user")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.param("username", "jojo")
.param("age", "18")
.param("ageTo", "60")
.param("xxx", "yyy")
.param("size", "15")
.param("page", "3")
.param("sort", "age,desc")
).andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(3))
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}
Controller中用一个POJO对象直接接收请求参数
@GetMapping
public List<User> query(UserQueryCondition condition,
@PageableDefault(page = 2, size = 17, sort = "username,asc") Pageable pageable) {
System.out.println(ReflectionToStringBuilder.toString(condition, ToStringStyle.MULTI_LINE_STYLE));
System.out.println(pageable.getPageSize());
System.out.println(pageable.getPageNumber());
System.out.println(pageable.getSort());
List<User> users = new ArrayList<>();
users.add(new User());
users.add(new User());
users.add(new User());
return users;
}
发送json字符串
@Test
public void whenCreateSuccess() throws Exception {
Date date = new Date();
System.out.println(date.getTime());
String content = "{\"username\":\"tom\",\"password\":null,\"birthday\":" + date.getTime() + "}";
String reuslt = mockMvc.perform(post("/user")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value("1"))
.andReturn().getResponse().getContentAsString();
System.out.println(reuslt);
}
Controller中用@RequestBody标注对象接收JSON参数
@PostMapping
public User create(@RequestBody User user) {
System.out.println(user.getId());
System.out.println(user.getUsername());
System.out.println(user.getPassword());
System.out.println(user.getBirthday());
user.setId("1");
return user;
}
jsonPath表达式
jsonPath表达式主要是对返回的json数据做一个判断
jsonPath("$.length()").value(3)
:返回长度为3的数组
jsonPath("$.username").value("tom")
:返回的username属性值为tom
@Test
public void whenGetInfoSuccess() throws Exception {
String result = mockMvc.perform(get("/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("tom"))
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}
指定返回http码
@Test
public void whenGetInfoFail() throws Exception {
mockMvc.perform(get("/user/a")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().is4xxClientError());
}
指定返回的是4xx的错误码
测试文件上传
package com.imooc.security.demo.web.controller;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.junit.Assert.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @Author 张柳宁
* @Description
* @Date Create in 2018/3/20
* @Modified By:
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@WebAppConfiguration("src/main/resources")
public class FileControllerTest {
private MockMvc mockMvc;
@Autowired
private WebApplicationContext wc;
@Before
public void setUp() throws Exception {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wc).build();
}
@Test
public void whenUploadSuccess() throws Exception {
String result = mockMvc.perform(fileUpload("/file")
.file(new MockMultipartFile("file", "test.txt", "multipart/form-data", "hello upload".getBytes("UTF-8"))))
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}
}
- 文件上传下载的控制器
package com.imooc.web.controller;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.imooc.dto.FileInfo;
import org.apache.commons.io.IOUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* @author zhailiang
*/
@RestController
@RequestMapping("/file")
public class FileController {
private String folder = "/Users/zhangliuning/Gitee/workspace/imooc-security/imooc-security-demo/target";
@PostMapping
public FileInfo upload(MultipartFile file) throws Exception {
System.out.println(file.getName());
System.out.println(file.getOriginalFilename());
System.out.println(file.getSize());
File localFile = new File(folder, new Date().getTime() + ".txt");
file.transferTo(localFile);
return new FileInfo(localFile.getAbsolutePath());
}
@GetMapping("/{id}")
public void download(@PathVariable String id, HttpServletRequest request, HttpServletResponse response) throws Exception {
try (InputStream inputStream = new FileInputStream(new File(folder, id + ".txt"));
OutputStream outputStream = response.getOutputStream()) {
response.setContentType("application/x-download");
response.addHeader("Content-Disposition", "attachment;filename=test.txt");
IOUtils.copy(inputStream, outputStream);
outputStream.flush();
}
}
}
@JsonView
在Restful API返回结果的时候,同一个对象对于不同的接口,会有不同的返回内容。
会选取对象中的部分字段进行返回,如果不需要的字段空着,或者新建对象,其实都不怎么合适。
这个时候就需要用到@JsonView
。
基本思路就是为返回对象设置分组,然后在Controller
中配置需要的组
举例:
- dto
public class User {
public interface UserSimpleView {};
public interface UserDetailView extends UserSimpleView {};
private String id;
private String username;
private String password;
private Date birthday;
@JsonView(UserSimpleView.class)
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@JsonView(UserDetailView.class)
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@JsonView(UserSimpleView.class)
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@JsonView(UserSimpleView.class)
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
}
UserDetailView
继承了UserSimpleView
,表示UserSimpleView
显示的字段UserDetailView
也要显示
- controller
@GetMapping
@JsonView(User.UserSimpleView.class)
public List<User> query(UserQueryCondition condition,
@PageableDefault(page = 2, size = 17, sort = "username,asc") Pageable pageable) {
......
}
请求表单与请求JSON的区别
对于键值对的请求两者区别不大
对于复杂对象,则必须使用json
表单请求可以get和post,json请求必须上post
关于日期格式
在后台中的日期格式,如果需要返回日期数据给前端
一般建议使用时间戳,不进行日期格式转换
因为后台面对的服务很多,不合适为特定的前端进行格式转化
请求参数校验
在请求参数对象上添加
@NotBlank(message = "密码不能为空")
private String password;
@Past(message = "生日必须是过去的时间")
private Date birthday;
等校验类
在控制器参数前添加@Valid
public User create(@Valid @RequestBody User user)
BindingResult
处理请求校验不通过的情况,做一些后续处理
@PostMapping
public User create(@Valid @RequestBody User user,BindingResult error) {
if (error.hasErrors()){
error.getAllErrors().stream().forEach(err -> System.out.println(err));
}
System.out.println(user.getId());
System.out.println(user.getUsername());
System.out.println(user.getPassword());
System.out.println(user.getBirthday());
user.setId("1");
return user;
}
BindingResult
对象与@Valid
配合,如果校验不通过,从BindingResult
对象获取异常信息
自定义校验注解
/**
*
*/
package com.imooc.security.demo.validator;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
/**
* @author zhailiang
*
*/
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MyConstraintValidator.class)
public @interface MyConstraint {
String message();
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
/**
*
*/
package com.imooc.security.demo.validator;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import com.imooc.security.demo.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
/**
* @author zhailiang
*
*/
public class MyConstraintValidator implements ConstraintValidator<MyConstraint, Object> {
@Autowired
private HelloService helloService;
@Override
public void initialize(MyConstraint constraintAnnotation) {
System.out.println("my validator init");
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
helloService.greeting("tom");
System.out.println(value);
return true;
}
}
@MyConstraint
就可以和标准校验注解一样进行使用了
@MyConstraint(message = "这是一个测试")
private String username;
SpringBoot的默认异常处理与控制器增强
当服务端产生异常的时候,如404、500等http异常
如果检测到是通过浏览器发起的请求,则返回一个html页面
如果检测到不是通过浏览器发送的请求,返回一个json串,字段为:timestamp、status、error、message、path
默认错误处理在SpringBoot的BasicErrorController
中
自定义校验异常
当Controller方法的参数对象校验未通过的时候,如果没有BindingResult捕获异常信息,那么会直接返回客户端异常,状态码为400,请求格式不对。
但此时是不能很好的向客户端反应出到底是什么问题的,到底是哪个字段的什么情况导致的格式不对,这个最好还是告诉客户的。这个时候就需要自定义异常的处理。
Step 1、自定义异常
/**
*
*/
package com.imooc.security.demo.exception;
/**
* @author zhailiang
*
*/
public class UserNotExistException extends RuntimeException {
/**
*
*/
private static final long serialVersionUID = -6112780192479692859L;
private String id;
public UserNotExistException(String id) {
super("user not exist");
this.id = id;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
Step 2、自定义控制器增强类
/**
*
*/
package com.imooc.security.demo.web.controller;
import java.util.HashMap;
import java.util.Map;
import com.imooc.security.demo.exception.UserNotExistException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
/**
* @author zhailiang
*/
@ControllerAdvice
public class ControllerExceptionHandler {
@ExceptionHandler(UserNotExistException.class)
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Map<String, Object> handleUserNotExistException(UserNotExistException ex) {
Map<String, Object> result = new HashMap<>();
result.put("id", ex.getId());
result.put("message", ex.getMessage());
return result;
}
}
此时,在程序的任何地方,只要抛出指定的异常:UserNotExistException
都能返回指定的json数据,且http状态码为HttpStatus.INTERNAL_SERVER_ERROR
,即 500
@GetMapping("/{id:\\d+}")
@JsonView(User.UserDetailView.class)
public User getInfo(@PathVariable String id) {
if ("1".equals(id)){
throw new UserNotExistException(id);
}
System.out.println("进入getInfo服务");
User user = new User();
user.setUsername("tom");
user.setId(id);
return user;
}
刚刚是对返回json的情况做了自定义异常处理,如果是页面的话,在classes类路径下,创建一个resources/error目录,以异常码为文件名创建html页面,
那么当指定的异常产生的时候,就会返回指定的html页面
手动控制返回状态
@ResponseStatus(HttpStatus.OK)
,可以在控制器方法中添加此注解
或者使用ResponseEntity
包裹返回的对象,设置状态
@RequestMapping(method=RequestMethod.POST, consumes="application/json")
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<Spittle> saveSpittle(@RequestBody Spittle spittle, UriComponentsBuilder ucb) {
Spittle saved = spittleRepository.save(spittle);
HttpHeaders headers = new HttpHeaders();
URI locationUri = ucb.path("/spittles/")
.path(String.valueOf(saved.getId()))
.build()
.toUri();
headers.setLocation(locationUri);
ResponseEntity<Spittle> responseEntity = new ResponseEntity<Spittle>(saved, headers, HttpStatus.CREATED);
return responseEntity;
}
ResponseEntity
是一种@ResponseBody
的替代方案
除了返回元数据外,还返回HTTP状态
服务拦截
一般用于请求/响应的日志记录、权限控制等
上述两点在微服务中使用网关也能做,但并不是每个Restful服务都是在网关中
所以还是有必要了解学习一下的
对于Spring而言,一般就AOP、拦截器两种方式
如果使用Servlet自己的标准,可以使用Filter过滤器
过滤器:Filter
记录请求/响应的时间
/**
*
*/
package com.imooc.security.demo.web.filter;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Date;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
/**
* @author zhailiang
*
*/
@Component
public class TimeFilter implements Filter {
@Override
public void destroy() {
System.out.println("time filter destroy");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("time filter start");
long start = new Date().getTime();
chain.doFilter(request, response);
System.out.println("time filter 耗时:"+ (new Date().getTime() - start));
System.out.println("time filter finish");
}
@Override
public void init(FilterConfig arg0) throws ServletException {
System.out.println("time filter init");
}
}
过滤器在SpringBoot的注册方式有两种
1、注册为Spring Bean,只要能过被@SpringBoot扫描到即可
2、注册到FilterRegistrationBean
第三方过滤器一般用第二种方式进行注册
注意:Filter在MockMVC测试中不会生效
必须启动服务后,通过工具发送请求才行
第二种注册方式如下:
/**
*
*/
package com.imooc.security.demo.web.config;
import java.util.ArrayList;
import java.util.List;
import com.imooc.security.demo.web.filter.TimeFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* @author zhailiang
*/
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Bean
public FilterRegistrationBean timeFilter() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
TimeFilter timeFilter = new TimeFilter();
registrationBean.setFilter(timeFilter);
List<String> urls = new ArrayList<>();
urls.add("/*");
registrationBean.setUrlPatterns(urls);
return registrationBean;
}
}
过滤器中不需要添加@Component
缺点:不知道该请求是发往哪个控制器的
Spring 拦截器能做到这个需求
拦截器:Interceptor
/**
*
*/
package com.imooc.web.interceptor;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Component
public class TimeInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println("preHandle");
System.out.println(((HandlerMethod)handler).getBean().getClass().getName());
System.out.println(((HandlerMethod)handler).getMethod().getName());
request.setAttribute("startTime", new Date().getTime());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("postHandle");
Long start = (Long) request.getAttribute("startTime");
System.out.println("time interceptor 耗时:"+ (new Date().getTime() - start));
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
System.out.println("afterCompletion");
Long start = (Long) request.getAttribute("startTime");
System.out.println("time interceptor 耗时:"+ (new Date().getTime() - start));
System.out.println("ex is "+ex);
}
}
注意:如果在@ControllerAdvice中已经把异常捕获了
那么在拦截器中是收不到异常的,即ex为null
注册拦截器
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired
private TimeInterceptor timeInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(timeInterceptor);
}
}
问题:无法拿到方法的参数值
AOP能做到
切片:AOP
package com.imooc.security.demo.web.aspect;
import java.util.Date;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* @author zhailiang
*
*/
@Aspect
@Component
public class TimeAspect {
@Around("execution(* com.imooc.security.demo.web.controller.UserController.*(..))")
public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("time aspect start");
Object[] args = pjp.getArgs();
for (Object arg : args) {
System.out.println("arg is "+arg);
}
long start = new Date().getTime();
Object object = pjp.proceed();
System.out.println("time aspect 耗时:"+ (new Date().getTime() - start));
System.out.println("time aspect end");
return object;
}
}
拦截顺序
image.pngFilter—>Interceptor—>ControllerAdvice—>Aspect—>Controller
Swagger
- 依赖
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.8.0</version>
</dependency>
- 配置
package org.zln.example.springbootswagger01.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class Swagger2Configuration {
@Bean
public Docket buildDocket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(buildApiInf())
.select()
.apis(RequestHandlerSelectors.basePackage("org.zln.example.springbootswagger01.web"))//要扫描的API(Controller)基础包
.paths(PathSelectors.any())
.build();
}
private ApiInfo buildApiInf() {
return new ApiInfoBuilder()
.title("Spring Boot中使用Swagger2 UI构建API文档")
.contact("土豆")
.version("1.0")
.build();
}
}
如果数默认配置的话,只要在启动类上添加个@EnableSwagger2
即可
在我们的控制器方法、请求参数、返回对象中,使用若干注解进行描述
启动服务后,在 http://ip:port/swagger-ui.html页面中就能看到文档
@Api(value = "计算服务", description = "简单的计算服务,提供加减乘除运算API")
添加在控制器中,描述控制器中的方法类别
@ApiOperation("加法运算")
添加在方法上,描述具体的方法
@ApiParam("被除数")
添加在基本类型参数上
@ApiModel("用户模型")
添加在请求响应对象类上
@ApiModelProperty("ID")
添加在请求响应对象的成员变量上
WireMock
WireMock用于伪造服务
从官网下载WireMock服务器,其实就是一个jar包,然后在本地启动
$ java -jar wiremock-standalone-2.14.0.jar
--port
: Set the HTTP port number e.g. --port 9999
--https-port
: If specified, enables HTTPS on the supplied port.
--bind-address
: The IP address the WireMock server should serve from. Binds to all local network adapters if unspecified.
--https-keystore
: Path to a keystore file containing an SSL certificate to use with HTTPS. The keystore must have a password of “password”. This option will only work if --https-port
is specified. If this option isn’t used WireMock will default to its own self-signed certificate.
--keystore-password
: Password to the keystore, if something other than “password”.
--https-truststore
: Path to a keystore file containing client certificates. See https and proxy-client-certs for details.
--truststore-password
: Optional password to the trust store. Defaults to “password” if not specified.
--https-require-client-cert
: Force clients to authenticate with a client certificate. See https for details.
--verbose
: Turn on verbose logging to stdout
--root-dir
: Sets the root directory, under which mappings
and __files
reside. This defaults to the current directory.
--record-mappings
: Record incoming requests as stub mappings. See record-playback.
--match-headers
: When in record mode, capture request headers with the keys specified. See record-playback.
--proxy-all
: Proxy all requests through to another base URL e.g. --proxy-all="http://api.someservice.com"
Typically used in conjunction with --record-mappings
such that a session on another service can be recorded.
--preserve-host-header
: When in proxy mode, it passes the Host header as it comes from the client through to the proxied service. When this option is not present, the Host header value is deducted from the proxy URL. This option is only available if the --proxy-all
option is specified.
--proxy-via
: When proxying requests (either by using –proxy-all or by creating stub mappings that proxy to other hosts), route via another proxy server (useful when inside a corporate network that only permits internet access via an opaque proxy). e.g. --proxy-via webproxy.mycorp.com
(defaults to port 80) or --proxy-via webproxy.mycorp.com:8080
--enable-browser-proxying
: Run as a browser proxy. See browser-proxying.
--no-request-journal
: Disable the request journal, which records incoming requests for later verification. This allows WireMock to be run (and serve stubs) for long periods (without resetting) without exhausting the heap. The --record-mappings
option isn’t available if this one is specified.
--container-threads
: The number of threads created for incoming requests. Defaults to 10.
--max-request-journal-entries
: Set maximum number of entries in request journal (if enabled). When this limit is reached oldest entries will be discarded.
--jetty-acceptor-threads
: The number of threads Jetty uses for accepting requests.
--jetty-accept-queue-size
: The Jetty queue size for accepted requests.
--jetty-header-buffer-size
: The Jetty buffer size for request headers, e.g. --jetty-header-buffer-size 16384
, defaults to 8192K.
--async-response-enabled
: Enable asynchronous request processing in Jetty. Recommended when using WireMock for performance testing with delays, as it allows much more efficient use of container threads and therefore higher throughput. Defaults to false
.
--async-response-threads
: Set the number of asynchronous (background) response threads. Effective only with asynchronousResponseEnabled=true
. Defaults to 10.
--extensions
: Extension class names e.g. com.mycorp.HeaderTransformer,com.mycorp.BodyTransformer. See extending-wiremock.
--print-all-network-traffic
: Print all raw incoming and outgoing network traffic to console.
--global-response-templating
: Render all response definitions using Handlebars templates.
--local-response-templating
: Enable rendering of response definitions using Handlebars templates for specific stub mappings.
--help
: Show command line help
设置mapping与文件
/**
*
*/
package com.imooc.security.demo.web.wiremock;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.configureFor;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.removeAllMappings;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
import java.io.IOException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.core.io.ClassPathResource;
/**
* @author zhailiang
*
*/
public class MockServer {
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
configureFor(8062);
removeAllMappings();
mock("/order/1", "01");
mock("/order/2", "02");
}
private static void mock(String url, String file) throws IOException {
ClassPathResource resource = new ClassPathResource("mock/response/" + file + ".txt");
String content = StringUtils.join(FileUtils.readLines(resource.getFile(), "UTF-8").toArray(), "\n");
stubFor(get(urlPathEqualTo(url)).willReturn(aResponse().withBody(content).withStatus(200)));
}
}
txt文件中其实就是json字符串数据,如
{
"id":1,
"type":"C"
}
异步处理
image.png对于Web容器而言,其能够对外提供的连接数是有限的,
例如:我们的Tomcat设定了1000个线程,在同步处理的模型中,当这1000个线程都在处理外部请求的时候,新的请求就无法被处理了。
为了服务更多的外部请求,我们的主线程收到请求后将请求交给副线程处理,
这个时候主线程能够继续接收外部的请求。
大大提高服务的负载能力
Runnable的方式
import org.apache.commons.lang.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import java.util.concurrent.Callable;
@RestController
public class AsyncController {
private Logger logger = LoggerFactory.getLogger(AsyncController.class);
@RequestMapping("/order")
public Callable<String> order() throws Exception {
logger.info("主线程开始");
Callable<String> result = new Callable<String>() {
@Override
public String call() throws Exception {
logger.info("副线程开始");
Thread.sleep(1000);//模拟业务逻辑
logger.info("副线程返回");
return "success";
}
};
logger.info("主线程返回");
return result;
}
}
2018-03-20 22:46:04.846 INFO 8125 --- [nio-8060-exec-8] c.i.s.demo.web.async.AsyncController : 主线程开始
2018-03-20 22:46:04.846 INFO 8125 --- [nio-8060-exec-8] c.i.s.demo.web.async.AsyncController : 主线程返回
2018-03-20 22:46:04.846 INFO 8125 --- [ MvcAsync4] c.i.s.demo.web.async.AsyncController : 副线程开始
2018-03-20 22:46:05.851 INFO 8125 --- [ MvcAsync4] c.i.s.demo.web.async.AsyncController : 副线程返回
从日志中可以看出,主线程收到请求后立马返回,可以接着处理新的请求
真正的业务处理数放在副线程中的。
而对外部而言,就好像这是一个同步请求的接口一样,完全感知不到任何区别
Runnable方式下的异步处理,副线程是由主线程调起的
在复杂环境下,如下图
image.png请求和返回在两个线程中,此时Runnable的方式就不合适了
Runnable方式要求副线程是由主线程开启的
使用DeferredResult
可以达到上述目标
DeferredResult
@GetMapping("/order3")
public DeferredResult<String> order3() {
logger.info("主线程开始");
DeferredResult<String> result = new DeferredResult<>();
// 异步调用
LongTimeAsyncCallService longTimeAsyncCallService = new LongTimeAsyncCallService();
longTimeAsyncCallService.makeRemoteCallAndUnnowWhenFinish(new LongTimeTaskCallback() {
@Override
public void callback(Object result1) {
logger.info("异步调用完成");
result.setResult(String.valueOf(result1));
}
});
logger.info("主线程返回");
return result;
}
@GetMapping("/order4")
public DeferredResult<String> order4() {
logger.info("主线程开始");
DeferredResult<String> deferredResult = new DeferredResult<>();
TaskService taskService = new TaskService();
CompletableFuture.supplyAsync(taskService::execute).whenCompleteAsync((result, throwable) -> deferredResult.setResult(result));
logger.info("主线程结束");
return deferredResult;
}
interface LongTimeTaskCallback {
void callback(Object result);
}
class LongTimeAsyncCallService {
private Logger logger = LoggerFactory.getLogger(LongTimeAsyncCallService.class);
private final int corePollSize = 4;
private final int needSeconds = 3;
private Random random = new Random();
private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(corePollSize);
public void makeRemoteCallAndUnnowWhenFinish(LongTimeTaskCallback callback) {
logger.info("完成任务需要:{}秒", needSeconds);
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
callback.callback("长时间异步调用完成");
}
}, needSeconds, SECONDS);
}
}
class TaskService {
private Logger logger = LoggerFactory.getLogger(TaskService.class);
public String execute() {
try {
logger.info("模拟异步执行的过程");
Thread.sleep(2000);
logger.info("异步调用结束");
return "异步执行的结果";
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
}
WebAsyncTask//TODO
这是另一种异步调用方式
异步拦截器配置
当使用异步的方式进行处理的时候,拦截器的配置是不一样的
覆盖
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
......
}
}
此方法
调用registerCallableInterceptors
或registerDeferredResultInterceptors
,根据实际情况进行注册
包括异步调用的若干配置,如异步调用的超时实践,也可以通过configurer
的相关方法进行配置
另一个就是线程池的配置,使用Runnable方式进行副线程调用的时候,默认每次都是开启新的线程,我们可以使用configurer
配置一个线程池
完整代码
关于Spring MVC开发Restful接口的完整代码
网友评论