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类

浙公网安备 33010602011771号