第三章 使用SpringMVC开发Restful API
1、编写Restful API的测试用例
package security.demo;
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.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {
/**
* 伪造web请求
*/
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
/**
* 每个测试用例执行前执行的方法
*/
@Before
public void setup(){
//伪造一个web请求
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
/**
* 发起一个web请求的测试
*/
@Test
public void whenQuerySuccess() throws Exception {
//发一个模拟的请求,然后对请求返回的内容进行判断
mockMvc.perform(MockMvcRequestBuilders.get("/userModel")//发一个get类型的请求
.param("username","哈哈")//请求的参数
.param("age","18")//请求的参数
.param("size","15")//分页的参数
.param("page","3")//分页的参数
.param("sort","age,desc")//分页排序的参数
.contentType(MediaType.APPLICATION_JSON_UTF8))//发送内容是JSON格式的
.andExpect(MockMvcResultMatchers.status().isOk())//期望返回的状态码是200
.andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3));//期望返回的JSON内容的长度是3
}
}
2、使用注解声明Restful API
(1):RestController表明此Controller提供的是RestAPI。
(2):RequestMapping及其变体,用来映射http请求url到java的方法上。
(3):RequestParam映射请求中的参数到java方法的参数上。
(4):PageableDefault指定分页参数默认值。
3、在Restful API中传递参数
4、用户详情服务(3-3节内容)
(1):@PathVariable映射url中的参数到java方法的参数上。
(2):在url声明中使用正则表达式。
/**
* @PathVariable的使用 加上正则表达式(表示参数只能是数字)
* @return
*/
@GetMapping("/user/{id:\\d+}")
public User getUserInfo(@PathVariable(value = "id") String id){
User user = new User();
user.setUsername("tom");
return user;
}
(3):JsonView控制方法返回的json内容。
使用步骤:
①在实体类中使用接口来声明多个视图。
②在值对象的get方法上指定视图。(即用来指定该字段可以显示到哪个视图上。)
①和②的完整示例
package security.demo.dto;
import com.fasterxml.jackson.annotation.JsonView;
import lombok.Data;
//@Data
public class User {
private String username;
private String password;
/**
* 声明简单视图接口
*/
public interface UserSimpleView{};
/**
* 声明详情视图接口
* 因为UserDetailView extends UserSimpleView
* 所以UserDetailView会显示UserSimpleView中显示的所有字段
*/
public interface UserDetailView extends UserSimpleView{};
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
/**
* 设定用户名只在简单视图显示
* @return
*/
@JsonView(UserSimpleView.class)
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
/**
* 设定密码只在详情视图显示
* @return
*/
@JsonView(UserDetailView.class)
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
③在Controller的方法上指定视图。(即用来指定哪些字段可以显示到返回的视图中。)
package security.demo.web.controller;
import com.fasterxml.jackson.annotation.JsonView;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.*;
import security.demo.dto.User;
import security.demo.dto.UserQueryCondition;
import java.util.ArrayList;
import java.util.List;
/**
* @author chenlc
*/
@RestController
public class UserController {
/**
* 使用RequestParam接收参数
* 使用了简单视图,只返回用户名
* @param nickname
* @return
*/
@JsonView(User.UserSimpleView.class)
@RequestMapping(value = "/user",method = RequestMethod.GET)
public List<User> query(@RequestParam(value = "username",required = false,defaultValue = "tom") String nickname){
List<User> users = new ArrayList<>();
users.add(new User("AA","123"));
users.add(new User("BB","456"));
users.add(new User("CC","789"));
System.out.println("nickname: "+nickname);
return users;
}
/**
* 使用实体类接收参数
* @param user
* @param pageable 用来接收分页参数
* @return
*/
@RequestMapping(value = "/userModel",method = RequestMethod.GET)
public List<User> queryByModel(UserQueryCondition user,@PageableDefault(page = 2,size = 17,sort = "username,asc") Pageable pageable){
List<User> users = new ArrayList<>();
users.add(new User());
users.add(new User());
users.add(new User());
System.out.println("user: "+user);
System.out.println("pageable: "+pageable.getPageNumber());
System.out.println("pageable: "+pageable.getPageSize());
System.out.println("pageable: "+pageable.getSort());
return users;
}
// /**
// * @PathVariable的使用
// * @return
// */
// @GetMapping("/user/{id}")
// public User getUserInfo(@PathVariable(value = "id") String id){
// User user = new User();
// user.setUsername("tom");
// return user;
// }
/**
* @PathVariable的使用 加上正则表达式(表示参数只能是数字)
* 使用了详情视图,会返回用户名和密码
* @return
*/
@JsonView(User.UserDetailView.class)
@GetMapping("/user/{id:\\d+}")
public User getUserInfo(@PathVariable(value = "id") String id){
User user = new User();
user.setUsername("tom");
user.setPassword("000");
return user;
}
}
5、处理创建请求(3-4节内容)
(1)@RequestBody映射请求体到java方法的参数上。
当使用post方法发送请求体数据时,Controller中接收方法的参数上要加@RequestBody来映射请求体到参数上(参数一般是实体类的对象)。
(2)日期类型参数的处理。
前后端使用时间戳来传递时间。
(3)@Valid注解验证请求参数的合法性,BindingResult收集校验结果。
@Valid加在方法的参数上,用来校验前端传来的参数与后台的数据要求是否一致。
BindingResult作为方法的参数,用来收集校验的结果。
6、开发用户信息的修改和删除服务(3-5节内容)
(1)常用的校验注解使用
@NotNull 值不能为空
@Null 值必须为空
@Pattern(regex=) 字符串必须匹配正则表达式
@Email 字符串必须是Email地址
@Size 集合中元素数量必须在min和max之间
等等
(2)自定义校验的提示消息
在加注解时,给message字段设置自定义的校验提示消息,而不是使用默认的提示校验提示信息。例如:@NotBlank(message = "密码不能为空")
package security.demo.dto;
import com.fasterxml.jackson.annotation.JsonView;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import security.demo.validator.MyConstraint;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Past;
import java.util.Date;
//@Data
public class User {
/**
* id
*/
@ApiModelProperty("用户id")
private String id;
/**
* 用户名不能为空
*/
@MyConstraint(message = "用户名不能为空")
@ApiModelProperty("用户名")
private String username;
/**
* 密码不能为空
*/
@NotBlank(message = "密码不能为空")
@ApiModelProperty("用户密码")
private String password;
/**
* 必须是一个过去的时间
*/
@Past(message = "生日必须是一个过去的时间")
@ApiModelProperty("用户生日")
private Date birthDay;
/**
* 声明简单视图接口
*/
public interface UserSimpleView{};
/**
* 声明详情视图接口
* 因为UserDetailView extends UserSimpleView
* 所以UserDetailView会显示UserSimpleView中显示的所有字段
*/
public interface UserDetailView extends UserSimpleView{};
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
public User(String id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}
public User(String id, String username, String password, Date birthDay) {
this.id = id;
this.username = username;
this.password = password;
this.birthDay = birthDay;
}
/**
* 设定id只在简单视图显示
* @return
*/
@JsonView(UserSimpleView.class)
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
/**
* 设定用户名只在简单视图显示
* @return
*/
@JsonView(UserSimpleView.class)
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
/**
* 设定密码只在详情视图显示
* @return
*/
@JsonView(UserDetailView.class)
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
/**
* 设定生日只在简单视图显示
* @return
*/
@JsonView(UserSimpleView.class)
public Date getBirthDay() {
return birthDay;
}
public void setBirthDay(Date birthDay) {
this.birthDay = birthDay;
}
@Override
public String toString() {
return "User{" +
"id='" + id + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
", birthDay=" + birthDay +
'}';
}
}
(3)自定义校验注解
①、新建自定义的校验注解
package security.demo.validator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MyConstraintValidator.class)//表明这个注解具有校验作用,使用的校验逻辑由validatedBy指定的校验类来执行
public @interface MyConstraint {
/**
* 校验需要的提示信息
* @return
*/
String message();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
②、新建自定义的校验逻辑类
package security.demo.validator;
import org.springframework.beans.factory.annotation.Autowired;
import security.demo.service.HelloService;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* 自定义的校验类需要实现ConstraintValidator接口
* MyConstraint:是需要验证的注解
* String:需要验证的字段类型,这样该注解就只能用在String类型的参数上,如果设置成Object,则任何类型都能使用该注解
*/
public class MyConstraintValidator implements ConstraintValidator<MyConstraint,Object> {
@Autowired
private HelloService helloService;
/**
* 校验类初始化时执行
* @param constraintAnnotation
*/
@Override
public void initialize(MyConstraint constraintAnnotation) {
System.out.println("my custom validator init");
}
/**
* @param o 需要校验的值
* @param constraintValidatorContext
* @return
*/
@Override
public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
helloService.greeting("tom");
System.out.println("需要校验的值:"+o);
// return true;
return false;
}
}
③、将自定义的校验注解加到需要校验的字段上。
/**
* 用户名不能为空
*/
@MyConstraint(message = "用户名不能为空")
private String username;
7、Restful API错误处理机制(3-6节内容)
(1)SpringBoot默认的错误处理机制
①: 浏览器端发出的请求,springboot如果处理时有错误,会由BasicErrorController的errorHtml()方法来处理,然后返回页面;
②: 非浏览器发出的请求(如PostMan等),springboot如果处理时有错误,会由BasicErrorController的error()方法来处理,然后返回JSON格式的错误信息;
(2)自定义异常处理
①: 浏览器端发出的请求,出现异常时返回的页面是在src/main/resources/templates/error/404.html配置,状态码是多少的就配置以该状态码为名称的页面,并在页面中设置返回的信息。
②: 非浏览器发出的请求(如PostMan等),springboot如果处理时有异常的情况处理方法:
步骤一:创建一个异常处理类
package security.demo.exception;
import java.io.Serializable;
/**
* 自定义的处理用户不存在的异常处理
*/
public class UserNotExistException extends RuntimeException implements Serializable {
private static final long serialVersionUID = -4253671095865255861L;
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;
}
}
步骤二、创建一个异常的拦截处理器
package security.demo.web.controller;
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;
import security.demo.exception.UserNotExistException;
import java.util.HashMap;
import java.util.Map;
/**
* ControllerAdvice:处理Controller抛出的异常
*/
@ControllerAdvice
public class ControllerExceptionHandler {
/**
* 处理抛出来的UserNotExistException类型的异常
* @param exception
* @return
*/
@ExceptionHandler(UserNotExistException.class)
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Map<String,Object> handlerUserNotExistException(UserNotExistException exception){
Map<String,Object> result = new HashMap<>();
result.put("id",exception.getId());
result.put("message",exception.getMessage());
return result;
}
}
只要springboot方法里抛出UserNotExistException类型的异常就会由ControllerExceptionHandler配置的handlerUserNotExistException()方法来处理。
8、Restful API的拦截(以拦截记录时间为例)(3-7、8节内容)
(1)过滤器(Filter)
优点: filter里面是能够获取到(HttpServletRequest request)和响应(HttpServletResponse response),从request中也能获取到传入的参数信息;
缺点:无法知道是哪一个Controller类中的哪个方法被执行。
Filter随web应用的启动而启动,只初始化一次,随web应用的停止而销毁。
1.启动服务器时加载过滤器的实例,并调用init()方法来初始化实例;
2.每一次请求时都只调用方法doFilter()进行处理;
3.停止服务器时调用destroy()方法,销毁实例。
方式一:实现Filter类(有@Component注解)
package security.demo.web.filter;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import java.io.IOException;
import java.util.Date;
/**
* @Component 这个注解的目的是将TimeFilter交给容器来管理。
* 时间过滤器
*/
@Component
public class TimeFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("时间过滤器初始化!");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("时间过滤器开始执行过滤!");
// long startTime = new Date().getTime();
long startTime = System.currentTimeMillis();
//执行下一个过滤器
filterChain.doFilter(servletRequest,servletResponse);
System.out.println("TimeFilter的耗时: "+(System.currentTimeMillis() - startTime));
System.out.println("时间过滤器结束执行过滤!");
}
@Override
public void destroy() {
System.out.println("销毁时间过滤器!");
}
}
方式二:实现Filter((无@Component注解)
①先定义一个没有@Component注解的过滤器
package security.demo.web.filter;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import java.io.IOException;
/**
* 没有@Component注解的过滤器
*/
public class OtherTimeFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("OtherTimeFilter初始化!");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("OtherTimeFilter开始执行过滤!");
long startTime = System.currentTimeMillis();
//执行下一个过滤器
filterChain.doFilter(servletRequest,servletResponse);
System.out.println("OtherTimeFilter的耗时: "+(System.currentTimeMillis() - startTime));
System.out.println("OtherTimeFilter结束执行过滤!");
}
@Override
public void destroy() {
System.out.println("销毁OtherTimeFilter!");
}
}
②使用Filter配置类来管理没有@Component注解的过滤器
package security.demo.web.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import security.demo.web.filter.OtherTimeFilter;
/**
* Filter:无法获取处理请求的方法信息
* springboot中filter的配置和顺序执行:https://www.cnblogs.com/douJiangYouTiao888/p/9183095.html
* 配置类
*/
@Configuration
public class WebFilterConfig {
/**
* FilterRegistrationBean是用来注册Filter的类
* @return
*/
@Bean
public FilterRegistrationBean otherTimeFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
//将自定义的没有@Component注解的过滤器注册进来
registrationBean.setFilter(new OtherTimeFilter());
//设置过滤顺序
registrationBean.setOrder(2);
//设置过滤器的名字
registrationBean.setName("OtherTimeFilter");
//设置需要过滤的地址
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}
方式一和方式二的区别:
方式二没有使用@Component注解,而是使用配置类来注册过滤器,这样的好处正如在过滤器注册时的设置一样清晰:
①:可以设置过滤器在过滤器链的位置(顺序);
②:可以设置过滤器在过滤器链中的名称;
③:可以设置过滤器拦截的规则;
而使用@Component注解的过滤器则无法更好的实现上述细节。
(2)拦截器(Interceptor)
优点: 可获请求(HttpServletRequest request)和响应(HttpServletResponse response)对象,也可获取方法所在类的类名及方法名信息。
MethodParameter[] methodParameters = ((HandlerMethod)handler).getMethodParameters();
for (MethodParameter methodParameter : methodParameters) {
String parameterName = methodParameter.getParameterName();
// 只能获取参数的名称,不能获取到参数的值
//System.out.println("parameterName: " + parameterName);
}
缺点:从handler对象中只能获取参数的名称,不能获取到参数的值,所以无法获取处理请求的方法里的形参信息,(从request中可以间接能获取到传入的参数信息)。
它比filter的执行优先级低。
步骤一:需要先实现HandlerInterceptor
package security.demo.web.interceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Interceptor:可获取处理请求的方法信息
*/
@Component
public class TimeInterceptor implements HandlerInterceptor {
/**
* 处理请求的方法执行前运行此方法
* @param request
* @param response
* @param handler 处理请求的方法
* @return
* @throws Exception
*/
@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",System.currentTimeMillis());
return true;
}
/**
* 处理请求的方法执行后运行此方法
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle方法执行");
Long startTime = (Long)request.getAttribute("startTime");
System.out.println("Interceptor执行耗时: "+ (System.currentTimeMillis() - startTime));
}
/**
* 处理请求的方法执行完毕后运行此方法
* @param request
* @param response
* @param handler
* @param exception
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception) throws Exception {
System.out.println("afterCompletion方法执行");
Long startTime = (Long)request.getAttribute("startTime");
System.out.println("Interceptor执行耗时: "+ (System.currentTimeMillis() - startTime));
System.out.println("exception is :"+exception);
}
}
步骤二:新建一个Web的配置类继承WebMvcConfigurationSupport
package security.demo.web.config;
import org.springframework.beans.factory.annotation.Autowired;
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.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import security.demo.web.filter.OtherTimeFilter;
import security.demo.web.interceptor.TimeInterceptor;
/**
* Filter:无法获取处理请求的方法信息
* springboot中filter的配置和顺序执行:https://www.cnblogs.com/douJiangYouTiao888/p/9183095.html
* 配置类:配置过滤器和拦截器,如果同时配置了拦截器和过滤器,二者都会起作用
*/
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
@Autowired
private TimeInterceptor timeInterceptor;
/**
* FilterRegistrationBean是用来注册Filter的类
* @return
*/
@Bean
public FilterRegistrationBean otherTimeFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
//将自定义的没有@Component注解的过滤器注册进来
registrationBean.setFilter(new OtherTimeFilter());
//设置过滤顺序
registrationBean.setOrder(2);
//设置过滤器的名字
registrationBean.setName("OtherTimeFilter");
//设置需要过滤的地址
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
/**
* 注册拦截器
* @param registry
*/
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(timeInterceptor);
}
}
(3)切片(Aspect)(3-8节内容) (最常使用)
优点: 可以获取方法信息及方法里的参数信息。
缺点: 无法直接获取方法的请求及响应对象。
(通过下面代码可以间接获取方法的请求及响应对象)
ServletRequestAttributes attribute = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attribute.getRequest();
HttpServletResponse response = attribute.getResponse();
常使用在日志,事务,请求参数安全验证等
package security.demo.web.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class TimeAspect {
/**
* (* security.demo.web.controller.UserController.*(..)):
* 第一个星号表示任何类型的返回值,第二个星号是UserController类里任何的方法
* (..):表示方法里的任何参数
* 整个的意思是作用在UserController的任何方法上,且不论方法的参数是什么,并且不管返回的是什么类型的方法上(即UserController的所有方法上)
* ProceedingJoinPoint:ProceedingJoinPoint的对象包含了被拦截的方法的所有信息
*/
@Around("execution(* security.demo.web.controller.UserController.*(..))")
public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("time aspect start!");
//获取被拦截的方法
Object proceed = pjp.proceed();
//获取方法的参数的数组
Object[] args = pjp.getArgs();
for(Object arg : args){
System.out.println("参数是: "+arg);
}
System.out.println("time aspect end!");
return proceed;
}
}
三者在程序中的执行顺序:
①Filter ②Interceptor (③ControllerAdvice) ④Aspect
执行后的返回顺序:
controller -> aspect -> controllerAdvice -> Interceptor -> Filter
9、文件的上传下载
下载功能需要加入IO的jar包
<!-- 处理文件上传下载的IO依赖 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
代码:
package security.demo.web.controller;
import org.apache.commons.io.IOUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import security.demo.dto.FileInfo;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.UUID;
@RestController
@RequestMapping("/file")
public class FileController {
/**
* 文件上传
* @param file
* @return
*/
@PostMapping
public FileInfo update(MultipartFile file){
//上传时参数的名字
System.out.println(file.getName());
//原始的文件名
String fileOriginalName = file.getOriginalFilename();
//文件的后缀名
String fileSuffixName = fileOriginalName.substring(fileOriginalName.lastIndexOf("."));
//文件的大小
System.out.println(file.getSize());
String folder = "自己指定一个路径";
File uploadPath = new File(folder);
if(!uploadPath.exists()){
uploadPath.mkdirs();
}
File saveFilePath = new File(folder, UUID.randomUUID().toString()+fileSuffixName);
try {
file.transferTo(saveFilePath);
} catch (IOException e) {
e.printStackTrace();
}
FileInfo fileInfo = new FileInfo(saveFilePath.getAbsolutePath());
return fileInfo;
}
/**
* 文件下载,因为是写在响应response中的,所以是无返回值的
* @param request
* @param response
* @param id
*/
@GetMapping("/{id}")
public void download(HttpServletRequest request, HttpServletResponse response, @PathVariable("id") String id) throws IOException {
//输入流
InputStream inputStream = new FileInputStream(new File("文件存储路径","文件的名字(包含后缀)"));
//输出流
OutputStream outputStream = response.getOutputStream();
//设置响应中的内容类型为下载类型
response.setContentType("application/x-download");
//设置下载时文件的名字
response.addHeader("Content-Disposition","attachment;filename=(设定的文件名(包含后缀名))");
IOUtils.copy(inputStream,outputStream);
outputStream.flush();
//关闭流
inputStream.close();
outputStream.close();
}
}
10、异步处理REST服务
(1)使用Runnable异步处理Rest服务
在主线程中使用Callable来发起一个副线程来执行耗时的逻辑。(需要注意的是副线程必须要由主线程发起调用,即主副线程之间必须要由关联)
package security.demo.web.async;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.Callable;
/**
* @author chenlc
*/
@RestController
@Slf4j
public class AsyncController {
/**
* 模拟同步请求处理
* @return
* @throws InterruptedException
*/
@GetMapping("/synchroOrder")
public String synchroOrder() throws InterruptedException {
log.info("主线程开始运行!");
Thread.sleep(1000);
log.info("主线程结束返回!");
return "success";
}
/**
* 模拟异步请求处理
* 使用Runnable的Callable来异步处理Rest服务
* 副线程必须要由主线程发起调用,即主副线程之间必须要由关联
* @return
* @throws InterruptedException
*/
@GetMapping("/asyncOrder")
public Callable<String> asyncOrder() throws InterruptedException {
log.info("主线程开始运行!");
Callable result = new Callable<String>(){
@Override
public String call() throws Exception {
log.info("副线程开始运行!");
Thread.sleep(1000);
log.info("副线程结束返回!");
return "success";
}
};
log.info("主线程结束返回!");
return result;
}
}
(2)使用DeferredResult异步处理Rest服务
①:模拟一个消息队列和应用2
package security.demo.web.async;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 模拟一个消息队列
*/
@Component
@Slf4j
public class MockQueue {
/**
* 下单
*/
private String placeOrder;
/**
* 完成订单
*/
private String completeOrder;
public String getPlaceOrder() {
return placeOrder;
}
/**
* 模拟一个处理下单请求的应用
* @param placeOrder
* @throws InterruptedException
*/
public void setPlaceOrder(String placeOrder){
new Thread(() ->{
log.info("接到下单请求!");
//模拟处理下单请求
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//模拟完成订单请求
this.completeOrder = placeOrder;
log.info("下单请求处理完毕: " + placeOrder);
}).start();
}
public String getCompleteOrder() {
return completeOrder;
}
public void setCompleteOrder(String completeOrder) {
this.completeOrder = completeOrder;
}
}
②:新建一个DeferredResult的持有类
package security.demo.web.async;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.async.DeferredResult;
import java.util.HashMap;
import java.util.Map;
/**
* 用来在两个线程之间传递DeferredResult对象
*/
@Component
public class DeferredResultHolder {
/**
* String:作为订单号
* DeferredResult<String>:作为该订单的处理结果对象
*/
private Map<String, DeferredResult<String>> map = new HashMap<>();
public Map<String, DeferredResult<String>> getMap() {
return map;
}
public void setMap(Map<String, DeferredResult<String>> map) {
this.map = map;
}
}
③:创建消息队列的监听器
package security.demo.web.async;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import java.lang.annotation.Target;
/**
* 监听器:监听MockQueue中是否放入数据
*/
@Component
@Slf4j
public class QueueListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private MockQueue mockQueue;
@Autowired
private DeferredResultHolder deferredResultHolder;
/**
* 当服务启动后就开始监听mockQueue
* @param contextRefreshedEvent
*/
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
new Thread(() ->{ //使用new Thread是为了防止主线程被占用
//mockQueue中有值就处理,无值就睡1s
while (true){
if(StringUtils.isNotBlank(mockQueue.getCompleteOrder())){
String orderNumber = mockQueue.getCompleteOrder();
log.info("返回订单处理结果: "+orderNumber);
deferredResultHolder.getMap().get(orderNumber)
.setResult("placeOrderSuccess");//订单处理完成之后设置返回结果
mockQueue.setCompleteOrder(null);
}else{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
④:编写处理异步请求的方法
/**
* 使用DeferredResult来处理异步请求
* @return
* @throws InterruptedException
*/
@GetMapping("/deferredResultOrder")
public DeferredResult<String> deferredResultOrder() throws InterruptedException {
log.info("主线程开始运行!");
//随机生成一个8位的订单号
String orderNumber = RandomStringUtils.randomNumeric(8);
mockQueue.setPlaceOrder(orderNumber);
DeferredResult<String> result = new DeferredResult<>();
deferredResultHolder.getMap().put(orderNumber,result);
log.info("主线程结束返回!");
return result;
}
功能的梳理:
步骤一:AsyncController中处理异步请求的deferredResultOrder()方法接收到请求后,随机生成一个订单号,设置到消息队列和DeferredResult中。
步骤二:MockQueue消息队列的setPlaceOrder()方法收到下单请求后,模拟应用2来处理订单。
步骤三:步骤二执行完毕后,QueueListener监听器开始返回订单的处理结果,将订单的处理结果放到DeferredResultHolder的对象中,并清空消息队列。
步骤四:处理异步请求的方法返回给前端处理结果。
(3)异步处理配置
WebMvcConfigurationSupport的configureAsyncSupport()方法
/**
* 处理异步的配置
* @param configurer
*/
@Override
protected void configureAsyncSupport(AsyncSupportConfigurer configurer) {
//1、注册Callable异步拦截器
configurer.registerCallableInterceptors();
//2、注册DeferredResult异步拦截器
configurer.registerDeferredResultInterceptors();
}
11、使用Swagger自动生成接口文档
步骤一:demo模块的pom.xml中加入swageer依赖
<!-- swagger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!-- swagger-ui视图 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
步骤二:在启动类上加入@EnableSwagger2
步骤三:在实体类的字段上加@ApiModelProperty("用户id")
步骤四:在Controller中的方法上加@ApiOperation("查询用户详情")
步骤五:重新启动springboot
12、使用WireMock快速伪造Restful服务
提供伪造的服务供前端使用,使用方法见链接:WireMock使用总结_Funnee的博客-CSDN博客_wiremock