2019-2-24

关键词:SpringMVC和SpringBoot常见功能、Spring Security

 

一、《Spring Security开发安全的REST服务》视频笔记---part1、springmvc和SpringBoot部分

1、Spring boot单元测试

eg:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {

	@Autowired
	private WebApplicationContext wac;

	private MockMvc mockMvc;

	@Before
	public void setup() {
		mockMvc = MockMvcBuilders.webAppContextSetup(wac).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);
	}
	

	@Test
	public void whenQuerySuccess() throws Exception {
		String result = mockMvc.perform(
				get("/user").param("username", "jojo").param("age", "18").param("ageTo", "60").param("xxx", "yyy")
						// .param("size", "15")
						// .param("page", "3")
						// .param("sort", "age,desc")
						.contentType(MediaType.APPLICATION_JSON_UTF8))
				.andExpect(status().isOk()).andExpect(jsonPath("$.length()").value(3))
				.andReturn().getResponse().getContentAsString();
		
		System.out.println(result);
	}

	@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);
	}
	
	@Test
	public void whenGetInfoFail() throws Exception {
		mockMvc.perform(get("/user/a")
				.contentType(MediaType.APPLICATION_JSON_UTF8))
				.andExpect(status().is4xxClientError());
	}
	
	@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);
	}
	
	@Test
	public void whenCreateFail() 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);
	}
	
	@Test
	public void whenUpdateSuccess() throws Exception {
		
		Date date = new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
		System.out.println(date.getTime());
		String content = "{\"id\":\"1\", \"username\":\"tom\",\"password\":null,\"birthday\":"+date.getTime()+"}";
		String reuslt = mockMvc.perform(put("/user/1").contentType(MediaType.APPLICATION_JSON_UTF8)
				.content(content))
				.andExpect(status().isOk())
				.andExpect(jsonPath("$.id").value("1"))
				.andReturn().getResponse().getContentAsString();
		
		System.out.println(reuslt);
	}
	
	@Test
	public void whenDeleteSuccess() throws Exception {
		mockMvc.perform(delete("/user/1")
				.contentType(MediaType.APPLICATION_JSON_UTF8))
				.andExpect(status().isOk());
	}

}

  

2、请求url正则表达式

eg:

@PutMapping("/{id:\\d+}")

3、@JsonView注解

使用步骤:

(1)使用接口来声明多个视图

public class User {
	
	public interface UserSimpleView {};
	public interface UserDetailView extends UserSimpleView {};
	
	private String id;
	
	@MyConstraint(message = "这是一个测试")
	@ApiModelProperty(value = "用户名")
	private String username;
	
	@NotBlank(message = "密码不能为空")
	private String password;
	
	@Past(message = "生日必须是过去的时间")
	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;
	}
	
}

  

上面声明了UserSimpleView和UserDetailView两个接口。

 

(2)在值对象的get方法上指定视图

例如上述代码中,有两处不同:

@JsonView(UserSimpleView.class)
	public String getUsername() {
		return username;
	}

  和

@JsonView(UserDetailView.class)
	public String getPassword() {
		return password;
	}

  而且由于UserSimpleView和UserDetailView是继承关系,所以显示密码的地方也会显示用户名。

(3)在Controller方法上指定视图

例如:

@GetMapping
	@JsonView(User.UserSimpleView.class)
	@ApiOperation(value = "用户查询服务")
	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;
	}

  和

        @GetMapping("/{id:\\d+}")
	@JsonView(User.UserDetailView.class)
	public User getInfo(@ApiParam("用户id") @PathVariable String id) {
//		throw new RuntimeException("user not exist");
		System.out.println("进入getInfo服务");
		User user = new User();
		user.setUsername("tom");
		return user;
	}

  

4、@RequestBody映射请求体到java方法的参数

eg:

后台接收:

public User create(@RequestBody User user)

前端发送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);
}

5、日期类型参数的处理

后台统一返回时间戳,前端根据自己的需求将时间戳转成特定格式的日期时间。

eg:

Date date = new Date();
String content = "{\"username\":\"tom\",\"password\":null,\"birthday\":"+date.getTime()+"}";

6、@Valid注解和BindingResult验证请求参数的合法性并处理校验结果

愚蠢的逐个校验(代码重构时不方便维护):

if(StringUtils.isNotBlank(pwd)){

}

  可以使用@NotBlank:

public class User {
	
	public interface UserSimpleView {};
	public interface UserDetailView extends UserSimpleView {};
	
	private String id;
	
	@MyConstraint(message = "这是一个测试")
	@ApiModelProperty(value = "用户名")
	private String username;
	
	@NotBlank(message = "密码不能为空")
	private String password;
}

  

但如果仅仅只加这个注解是不行的!!!还要在控制器方法中配合@Valid注解使用:

        @PostMapping
	@ApiOperation(value = "创建用户")
	public User create(@Valid @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;
	}

  注意:如果没有加上BindingResult参数,且传进来的password为空,则该方法根本不会执行,而是直接报错!!!如果想要知道是发生了什么错误并且能进入方法进行处理,则:

        @PostMapping
	@ApiOperation(value = "创建用户")
	public User create(@Valid @RequestBody User user, BindingResult errors) {
    
                if(errors.hasErrors()){
                    errors.getAllErrors().stream().forEach(err->System.out.println(err.getDefaultMessage()));
                }

		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;
	}    

  

扩展:还有很多常用的验证注解在Hibernate Validator可以查看。

 

7、自定义校验注解

(1)创建注解(

注意:

@Constraint(validatedBy = MyConstraintValidator.class)

@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 { };

}

  

(2)定义刚刚这个注解的校验逻辑由谁来执行(泛型参数中,第一个是注解名,第二个是注解适合用在什么类型的参数)

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;
	}

}

(3)使用注解:

        @MyConstraint(message = "这是一个测试")
	private String username;

  

8、Spring Boot中默认的错误处理机制和自定义异常处理

可以使用chrome的一个插件来模拟app的请求:Restlet Client

SpringBoot默认的错误处理机制是:检测到是浏览器发出,则返回html;是app发出,则返回json字符串。源码在BasicErrorController

 浏览器自定义返回内容:如果想在指定错误发生(比如404)时返回指定html页面,可以在resources文件夹下再新建一个resources/error/404.html,再发生404时将显示这个页面。

客户端自定义返回内容:

①控制器方法内部抛出一个异常(可以是一个自定义的继承RuntimeException的异常类)

②定义一个处理控制器异常的类:

@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;
	}

}

  

9、Restful API的拦截的3种机制:

(1)过滤器(Filter)

@Component
public class TimeFilter implements Filter {

	/* (non-Javadoc)
	 * @see javax.servlet.Filter#destroy()
	 */
	@Override
	public void destroy() {
		System.out.println("time filter destroy");
	}

	/* (non-Javadoc)
	 * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
	 */
	@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");
	}

	/* (non-Javadoc)
	 * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
	 */
	@Override
	public void init(FilterConfig arg0) throws ServletException {
		System.out.println("time filter init");
	}

}

  假如需要将第三方的Filter加入到项目中(这些Filter不会有@Component注解),需要自己写配置类将该第三方Filter加入到Filter链中(该配置和在web.xml中配置filter标签效果一样,只不过SpringBoot不能用web.xml配置):

@Configuration
public class WebConfig {
	
	@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;
		
	}

}

  这种用法的局限是,filter属于j2ee的东西而不是spring的东西,所以filter内无法知道request是由哪个Controller的哪个方法处理的,如果想知道则要用第2种机制(Spring框架本身提供的)。

(2)拦截器(Interceptor)

@Component
public class TimeInterceptor implements HandlerInterceptor {

	/* (non-Javadoc)
	 * @see org.springframework.web.servlet.HandlerInterceptor#preHandle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object)
	 */
	@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;
	}

	/* (non-Javadoc)
	 * @see org.springframework.web.servlet.HandlerInterceptor#postHandle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object, org.springframework.web.servlet.ModelAndView)
	 */
	@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));

	}

	/* (non-Javadoc)
	 * @see org.springframework.web.servlet.HandlerInterceptor#afterCompletion(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object, java.lang.Exception)
	 */
	@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);

	}

}

  注意两个地方:

①上面afterCompletion方法中的Exception 参数在控制器方法抛出UserNotExistException异常时是无法获取的,因为前面有一个@ControllerAdvice修饰的类里面@ExceptionHandler(UserNotExistException.class)修饰的方法会提前获取到这个异常;若是其它没有被提前处理的异常则可以获取。

②拦截器和过滤器不同的地方是,这里拦截器已经用了@component注解,但依然还是需要配置,否则无法生效:

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
	
	@Autowired
	private TimeInterceptor timeInterceptor;
	
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(timeInterceptor);
	}


}

  拦截器有一个局限,就是上面preHandle方法中的handler参数只能拿到控制器处理的类名和方法名,但无法拿到方法参数的值。原因见源码DispatcherServlet类的doService方法里面调用了一个doDispatch方法。若想拿到则要用第三个机制。

(3)切片(Aspect)

Spring AOP简介:

切片(类):由“切入点”(注解)和“增强”(方法)组成。

切入点:1、在哪些方法上起作用;2、在什么时候起作用

增强:起作用时执行的业务逻辑。

eg:

@Aspect
@Component
public class TimeAspect {
	
	@Around("execution(* com.imooc.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;
	}

}  

 

总结1:过滤器、拦截器、切片的区别是:

①过滤器可以拿到原始的http请求和响应的信息,但是拿不到真正处理请求的那个方法的信息

②拦截器既可以拿到原始的http请求和响应的信息,也能拿到真正处理请求的那个方法的信息,但拿不到那个方法的参数的值

③切片能拿到那个方法的参数的值,但拿不到原始http请求响应对象。

总结2:拦截顺序(从外到内)和抛出异常顺序(从内到外,并且多了一个ControllerAdvice)是:

 

10、文件上传和下载

现在很多应用前后端分离,前端都是SPA,所以不会刷新页面,也不会有表单提交,大部分情况上传文件都是异步完成,即提交表单只是提交一个文件路径,然后文件的上传都是另外单做的。

public class FileInfo {
	
	public FileInfo(String path){
		this.path = path;
	}
	
	private String path;

	public String getPath() {
		return path;
	}

	public void setPath(String path) {
		this.path = path;
	}
	
}

  

@RestController
@RequestMapping("/file")
public class FileController {

	private String folder = "/Users/zhailiang/Documents/my/muke/inaction/java/workspace/github/imooc-security-demo/src/main/java/com/imooc/web/controller";

	@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();
		} 

	}

}

  

11、异步处理REST服务

异步处理的好处:主线程调用副线程后不需要等待,可以继续处理其它新的请求。

本节有三块内容:

(1)使用Runnable异步处理Rest服务

(2)使用DeferredResult异步处理Rest服务

(3)异步处理配置

 

同步方法处理(需要1秒左右):

@RestController
public class AsyncController {
	
	private Logger logger = LoggerFactory.getLogger(getClass());
	
	@RequestMapping("/order")
	public String order() throws Exception {
		logger.info("主线程开始");
                Thread.sleep(1000);
		logger.info("主线程返回");
		return "success";
	}
}

  

异步处理(父线程即Tomcat线程立刻返回不需要等待),服务器吞吐量可以提升:

@RestController
public class AsyncController {
	
	private Logger logger = LoggerFactory.getLogger(getClass());
	
	@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;
	}
}

  上面这种做法就属于(1)使用Runnable异步处理Rest服务,但这种方法有局限:副线程必须写在主线程内部。对于更复杂的企业级应用,需要使用DeferredResult异步处理Rest服务。

以这个为例:

在这个例子中,线程1负责发送,线程2负责监听,两者是隔离的,使用方法(1)无法实现,需要方法(2)。

下面会有4段代码:

用一个对象模拟上面的消息队列的代码、用一个Tomcat主线程接收请求的代码、监听处理结果并返回响应的线程2代码、用一个DeferredResultHolder将线程1处理完后得到的DeferredResult在线程2返回回去。

模拟消息队列:

@Component
public class MockQueue {

	private String placeOrder;

	private String completeOrder;
	
	private Logger logger = LoggerFactory.getLogger(getClass());

	public String getPlaceOrder() {
		return placeOrder;
	}

	public void setPlaceOrder(String placeOrder) throws Exception {
		new Thread(() -> {
			logger.info("接到下单请求, " + placeOrder);
			try {
				Thread.sleep(1000);
			} catch (Exception e) {
				e.printStackTrace();
			}
			this.completeOrder = placeOrder;
			logger.info("下单请求处理完毕," + placeOrder);
		}).start();
	}

	public String getCompleteOrder() {
		return completeOrder;
	}

	public void setCompleteOrder(String completeOrder) {
		this.completeOrder = completeOrder;
	}

}

  DeferredResultHolder :

@Component
public class DeferredResultHolder {
	
	private Map<String, DeferredResult<String>> map = new HashMap<String, DeferredResult<String>>();

	public Map<String, DeferredResult<String>> getMap() {
		return map;
	}

	public void setMap(Map<String, DeferredResult<String>> map) {
		this.map = map;
	}
	
}

  上面的Map<String, DeferredResult<String>> map中的key是订单号,value是订单处理结果。

接收请求的线程:

@RestController
public class AsyncController {
	
	@Autowired
	private MockQueue mockQueue;
	
	@Autowired
	private DeferredResultHolder deferredResultHolder;
	
	private Logger logger = LoggerFactory.getLogger(getClass());
	
	@RequestMapping("/order")
	public DeferredResult<String> order() throws Exception {
		logger.info("主线程开始");
		
		String orderNumber = RandomStringUtils.randomNumeric(8);
		mockQueue.setPlaceOrder(orderNumber);
		
		DeferredResult<String> result = new DeferredResult<>();
		deferredResultHolder.getMap().put(orderNumber, result);
		
		return result;

	}

}

  监听并返回响应的线程:

@Component
public class QueueListener implements ApplicationListener<ContextRefreshedEvent> {

	@Autowired
	private MockQueue mockQueue;

	@Autowired
	private DeferredResultHolder deferredResultHolder;
	
	private Logger logger = LoggerFactory.getLogger(getClass());

	@Override
	public void onApplicationEvent(ContextRefreshedEvent event) {
		new Thread(() -> {
			while (true) {

				if (StringUtils.isNotBlank(mockQueue.getCompleteOrder())) {
					
					String orderNumber = mockQueue.getCompleteOrder();
					logger.info("返回订单处理结果:"+orderNumber);
					deferredResultHolder.getMap().get(orderNumber).setResult("place order success");
					mockQueue.setCompleteOrder(null);
					
				}else{
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}

			}
		}).start();
	}
}

  上面这个方法里之所以单独启动一个线程运行,是因为这个监听器是监听容器启动的事件,若该方法一直循环则阻止容器正常启动。

最后就是内容(3)异步处理配置。前面配置过一个WebConfig类来拦截处理同步的请求(filter或者Interceptor),但如果是要拦截上面的异步请求,则配置方法不同:

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
	
	@Override
	public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
		configurer.。。()
	}

}

  比如如果是要拦截内容(1)的那个Callable的请求,就需要这样注册异步处理的拦截器:

configurer.registerCallableInterceptors(interceptors)

  

12、与前端开发并行工作

介绍两个工具:使用swagger自动生成html文档、使用WireMock(本身就是一个独立的服务器,可以接收前端的请求,然后模拟数据返回结果)快速伪造RESTful服务。

 了解三个swagger常用注解以及@EnableSwagger2即可。

WireMock例子:注意要先去官网下载WireMock的可运行jar包然后跑起来(作为独立的服务器),接着引入相关pom,才能开始开发

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)));
	}

}

 

二、java8新增时间api:LocalDateTime类

 

posted @ 2019-02-24 17:36  Z.Y.X  阅读(368)  评论(0编辑  收藏  举报