关键词: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(), " "); stubFor(get(urlPathEqualTo(url)).willReturn(aResponse().withBody(content).withStatus(200))); } }
二、java8新增时间api:LocalDateTime类