目录结构导航:
导航:
一. 项目初始化
1.1 简介:
- 我会从SpringMVC开发RESTfulAPI入手,讲解工作中的关于RESTful 相关的东西
1.2 连接数据库的相关问题
- 首先我们需要在xml中添加相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
- 数据库连接:
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/...
spring.datasource.username=root
spring.datasource.password=root
~~~java
- 集群Session管理(后面讲)
~~~java
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
</dependency>
刚开始的时候我们不一定需要使用,我们可以设置手动关闭:
在application.yml中进行设置:
spring.session.store-type:none
//将Session关闭
配置端口号:server.prot=8060
当我们引入SpringSecurity框架的时候,会默认启动身份验证,如果我们不想启动身份验证我们可以先手动关闭:
security.basic.enabled=false
//这样后访问系统内的接口就不会再自动身份验证啦~
后面我们会将这些全部打开,并进行相关的配置;
1.3 项目打包的问题
- 打包我们可以使用Maven的Build进行打包;一般打包出来的文件是不包含第三方jar包的,我们如果想打包一个可运行的WEB项目,可以这样做:
- 在xml中添加依赖:
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>1.3.3.RELEASE</version> </plugin> </plugins> <finalName>demo</finalName> //打包出来的名字 </build>
- 在xml中添加依赖:
当有了这些配置后,我们在target中就可以看到打包有两个文件:
一个以jar.original结尾的是原始jar包,不包含第三方的;
一个是可执行jar包,名字为demo.jar 它是可以直接执行的;
- 执行打包出来的jar包:
- 启用cmd命令行,先cd到项目的target下输入:java -jar demo.jar
- 输入后就会直接启动jar包,跟我们在项目中main函数启动的效果是一样的,我们可以直接在浏览器中请求接口;
二. 使用SpringMVC开发RESTful API : 查询
2.1 RESTful特点
-
学习内容:
- 使用SpringMVC 编写Restful API
- 使用SpringMVC处理其他web应用常见的需求和场景
- Restful API开发常用辅助框架
-
RESTful API特点:
- 用URL描述资源
- 使用HTTP方法描述行为。使用HTTP状态码来表示不同的结果
- 使用json交互数据
- RESTful只是一种风格,并不是强制的标准
在请求中只放资源,而不会表明做什么操作,通过HTTP状态进行对应
2.2 REST成熟度模型:
- 等级划分:
- 第0级: 使用HTTP作为传输方式
- 第1级: 引入资源概念,每个资源都有对应的URL
- 第2级: 使用HTTP方法进行不同的操作。使用HTTP状态码来表示不同的结果
- 第3级: 使用超媒体,在资源的表达中包含了链接信息;
2.3 编写第一个Restful API 查询
- 编写针对Restful API测试用例
- 首先添加依赖:
//<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </denpendency>
添加这个依赖后会将Spring的测试框架引入
- 测试用例代码: //快速的判断程序是不是按照我们所期望的那样运行
- @RunWith(SpringRunner.class) - @SpringBootTest - publci class UserControllerTest{ - @Autowired - private WebApplicationContext wac; - private MockMvc mockMVC; - @Befroe - public void setup(){ - mockMvc=MockMvcBuilders.webAppContextSetup(wac).build(); - } - @Test - public void whenQuerySuccess() throws Exception{ - mockMvc.perform(MockMvcRequestBuilders.get("/user") - . param("username","pojo") - .contentType(MediaType.APPLICATION_JSON_UTF8)) - .addExpect(MockMvcResultMathchers.status().isOk()) - .andExpect(MockMvcResultMathers.jsonPath("$.length()").value(3));}} //返回的结果json长度必须为3
- 首先添加依赖:
- 使用注解声明RestfulAPI
- 常用注解:
- @RestController 标明此Controller提供RestAPI
- @RequestMapping及其变体。映射http请求url到java方法
- @RequestParam 映射请求参数到java方法的参数
- @PageableDefault指定分页参数默认值
- 代码示例:
@RestController public class UserController{ @RequestMapping(value="/user",method=RequestMethod.GET) public List<User>query(@RequestParam String username){ //这里的@RequestParam 表示请求的参数必须带一个这样的参数username List<User> users=new ArrayList<>(); users.add(new User()); users.add(new User()); users.add(new User()); return users; } }
- @@RequestParam的问题:
- 在使用@RequestParam()中,如果参数和请求的值不一样,我们可以这样设置:@RequestParam(name=“username” String nickname),这样前端把username的值传入的时候,也可以将值给到nickname了,解决了参数名不一致的问题;
- 我们也可以不设置不一定要携带这个参数,设置如下:
@RequestParam(required=false,defaultVGFalue="tom") String nickname 这样设置的时候,即便没有给这个nickname值,也不会报错,同时还可以给到它一个默认的值
- 常用注解:
- 在RestfulAPI中传递参数
2.4 编写用户详情服务
- 概述:
- @PathVariable 映射url片段到java方法的参数
- 在url在声明中声依永正则表达式
- @JsonView控制json输出内容
- 测试用例代码:
@Test public void whenGenInfoSuccess() throws Exception{ mockMvc.perform(get("/user/1") .contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(status().isOk()) .andExpect(jaonPath("$.username").value("tom")) }
这里测试用例,限制了这个请求接口必须为GET请求,然后犯规的username是tom,用户id值为1的用户详细信息,这里将MockMvcRequestBuilders以及另外一个提取出来做为了一个静态方法,所以可以省略直接调用;
-
接口方法代码:
@RequestMapping(value="/user/{id}",method=RequestMethod.GET) public User getInfo(@PathVariable(name="id") String id){ //指定了name=id 那么后面的String类型的变量名可以随便取名,不一定为id User user=new User(); user.setUsername("tom"); return user; }
-
我们还可以进行限制,id值只能传入数字,使用正则表达式
@RequestMapping(value="/user/{id:\\d+}",method=RequestMethod.GET) public User getInfo(@PathVariable(name="id") String id){ //指定了name=id 那么后面的String类型的变量名可以随便取名,不一定为id User user=new User(); user.setUsername("tom"); return user; }
-
当id值为字母的时候,肯定是错误的,那么我们可以做一个错误测试用例,检测上面的正则表达式是否有效果;
@Test public void whenGetInfoFail() throws Exception{ mockMvc.perform(get("/user/a") .contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(status().is4xxClientError()); //服务器返回4..的状态比如404,400等都是在内 }
2.5 JosnView注解
-
使用步骤:
- 使用接口来声明多个视图
- 在值对象的get方法上指定视图
- 在Controller方法上指定视图
-
概述: JsonView 是什么,为什么要使用它?
- @JsonView是Spring中的一个注解,用于在不同的请求中返回不同的视图。例如,在请求/users中会返回一个包含基本用户信息的List,此时不应该把所有的用户密码等详情返回出来。但是,当请求某一个用户的详情/user/{id:\+d}时候,则需要返回该用户的所有信息。此时,可以通过JsonView注解来表明如何在不同的请求中返回什么样的视图。
-
代码示例:
- 使用接口声明多个视图,并在对象属性的get方法上指定视图
import org.hibernate.validator.constraints.NotBlank; import com.fasterxml.jackson.annotation.JsonView; public class User { //声明一般视图接口 只允许这个视图返回用户名属性 public interface UserSimpView{}; //声明完整视图接口 允许返回用户名密码属性 由于集成了一般视图接口 含义是拥有了一般视图所具有的返回属性 public interface UserDetailView extends UserSimpView{}; private Integer Id; private String userName; private String passWord; public User() { } public User(Integer Id, String userName, String passWord) { this.Id = Id; this.userName = userName; this.passWord = passWord; } @JsonView(UserSimpView.class) public Integer getId() { return Id; } public void setId(Integer id) { Id = id; } @JsonView(UserSimpView.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; } @Override public String toString() { return "User [Id=" + Id + ", userName=" + userName + ", passWord=" + passWord + "]"; } }
- 在Controller上指定视图:
@RestController @RequestMapping("/user") public class UserController { @GetMapping(produces="application/json;charset=UTF-8") @JsonView(UserDetailView.class) public List getUserAll(@RequestParam("token") String token){ List<User> userList = new ArrayList<>(); userList.add(new User(1,"二十岁以后特殊视图0","123456特殊视图")); userList.add(new User(2,"二十岁以后特殊视图1","qweqwe特殊视图")); userList.add(new User(3,"二十岁以后特殊视图2","asdgdd特殊视图")); return userList; }
- 编写测试类:
- 使用完整视图的代码如下:
@RestController @RequestMapping("/user") public class UserController { @GetMapping(produces="application/json;charset=UTF-8") @JsonView(UserDetailView.class) //完整视图,返回username,password public List getUserAll(@RequestParam("token") String token){ List<User> userList = new ArrayList<>(); userList.add(new User(1,"二十岁以后特殊视图0","123456完整视图")); userList.add(new User(2,"二十岁以后特殊视图1","qweqwe完整视图")); userList.add(new User(3,"二十岁以后特殊视图2","asdgdd完整视图")); return userList; }
- 使用特殊视图的代码如下:
@RestController @RequestMapping("/user") public class UserController { @GetMapping(produces="application/json;charset=UTF-8") @JsonView(UserSimpView.class) public List getUserAll(@RequestParam("token") String token){ List<User> userList = new ArrayList<>(); userList.add(new User(1,"二十岁以后特殊视图0","123456完整视图")); userList.add(new User(2,"二十岁以后特殊视图1","qweqwe完整视图")); userList.add(new User(3,"二十岁以后特殊视图2","asdgdd完整视图")); return userList; }
- 使用完整视图的代码如下:
- 使用接口声明多个视图,并在对象属性的get方法上指定视图
2.5 简化RequestMapping
-
我们可以使用变体简化:
- GetMapping("/user") 与 RequestMapping(method=RequestMethod.GET) //他们的含义是一样的
- 其他的也是同理,比如PostMapping() DeleteMapping等。。。
-
有测试用例的时候,我们在重构代码就能按着正确的路线走;
三. 使用SpringMVC开发RESTful API : 创建
3.1 处理创建请求的要点:
- @ReqeustBody映射请求到java方法的参数
- 日期类型参数的处理
- @Valid注解和BindingResult验证请求的合法性并处理校验结果
3.2 创建请求的测试用例代码
@Test
public void whenCreateSuccess() throws Exception{
String content="{\"username\":\"tom\",\"password\":null}";
mockMvc.perform(post("/user").contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content)) //这里是给接口发送请求的时候携带的参数,表示只给了username和password,而username有值,password没有值
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value("1"));
}}
3.3 Controller中创建的接口
@PostMapping
public User create(@RequestBody() User user){ //使用@RequestBody后才能将请求的参数映射到对象里面,它是将Post的请求体Body里面的值映射
user.setId("id");
return user;
}
3.4 日期参数类型的处理
-
在类中有生日Birthday的日期类型,我们应该如何去操作呢?
- 在前后端分离,不同渠道,用指定的返回格式很麻烦,我们不使用带格式的时间转换器,我们可以使用时间戳,前端来决定如何展示;
- 在我们的实体类中,比如生日: private Date birthday 是这样定义的,当前端传入的数据可以直接为时间戳,经过@ReqeustBody的映射自动转换后,就会变成时间格式了;当我们要返回给前端的时候,我们也可以继续以时间戳的形式返回;因为类上面有@RestController注解,那么返回给前端的就自动变成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; }
- 测试方法
@Test public void whenCreateSuccess() throws Exception{ Date date=new Date(); String content ="{\"username\":\"tom\",\"password\":null,\"birthday\":"+date.getTime}; //将时间戳信息给content String result=mockMvc.perform(post("/user").contentType(MediaType.APPLICATION_JSON_UTF8) .content(content)) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value("1")) .andReturn().getResponse().getContentAsString(); }
- 接口:
3.5 @Valid注解和BindingResult 验证请求参数的合法性并处理校验结果
- 介绍:
- 一定要验证用户传入的数据是否是有效的,有效的时候再执行逻辑,而每次这样写都会很繁琐,我们可以使用一些简便的方法进行限制,比如我们可以在实体类中的一个属性上加一个注解:@NotBlank 不为空
@NotBlank private String password;
- 我们还需要添加一个@Valid注解,在将请求体的数据转为对象的时候,就会判断类中的@NotBlank不为空;如果为空的话就会直接将请求打回,不会再进入请求体了;
@PostMapping public User create(@Valid @RequestBody User user){ user.setId("1"); return user; }
- 有的时候,我们需要将错误信息记录下来,那么是要进入请求体的;比如当谁调用了这个接口,虽然格式不对,我们也要将用户请求的操作记录到日志里面;我们可以使用BindingResult erros ,它会带着错误信息进入请求;
//返回的结果大致为: //显示内容:may not be empty //具体方法:error.getDefaultMessage()
- 接口改造:
@PostMapping public User create(@Valid @RequestBody User user,BindingResult errors){ if(errors.hasErrors()){ errors.getAllErrors().stream().forEach(errorm -> System.out.println(error.getDefaultMessage())); } user.setId("1"); return user; }
- 一定要验证用户传入的数据是否是有效的,有效的时候再执行逻辑,而每次这样写都会很繁琐,我们可以使用一些简便的方法进行限制,比如我们可以在实体类中的一个属性上加一个注解:@NotBlank 不为空
四. 使用SpringMVC开发RESTful API : 修改和删除
4.1 讲解点:
- 常用的验证注解
- 自定义消息
- 自定义校验注解
4.2 常用验证注解
-
图示1:
[外链图片转存失败(img-MNxuvF3S-1563862092757)(https://i.imgur.com/n2v52DS.jpg)] -
图示2:
[外链图片转存失败(img-XByUolvq-1563862092758)(https://i.imgur.com/fHrZxhi.jpg)]
4.3 代码
- 一年以后的时间(这是未来的时间):
Date date=new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli())
- 概述: 当我们在实体类User上的Date类型的字段birthday上添加注解@Past的时候,并且从@PutMapping接口中传入的值加了@Valid和@ReqeustBody注解后,它会进行判断birthday字段传入的时间是否为当前时间的过去,如果不为则会报错,如果加了BindingResult类,则会返回:must be in the past //必须为过去
- 说明:系统返回的是英文,我们可以自定义返回错误信息,比如:
@Past(message="生日必须是过去的时间") private Date birthday;
4.4 自定义注解验证
- 概述:很多时候,我们用不到这里面的注解,我们可以自定义注解,根据业务需要对传入的值进行判断;
- 操作: //3.5 修改和删除请求这里
- 先创建一个注解Annotation: MyConstraint
@Target({ElementType.METHOD,ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validateBy=MyConstrainvalidator.class) //锁定这个校验接口实现类 public @interface MyConstraint{ String message(); Class<?>[] groups[] default{}; Class<? extends Payload>[] payload() default{}; }
- 实现一个接口ConstraintValidator,并重写它的两个方法,一个是初始化方法,这个随意,可以不写,一个是校验方法isValid ,这里面可以写校验逻辑,如果校验通过了,则可以直接返回true;
//这里不用加@Component等注解,继承了这个接口就会自动加入Bean 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.geeting("tom"); //这里可以忽略,执行业务逻辑,我们平时可以在这里随便写,可以注入Service //这里的value是传入的值; return true; } }
- 使用
- 在实体类字段上添加注解:
@MyConstraint(message="这是一个测试") private String username;
- 直接测试即可
- 在实体类字段上添加注解:
- 先创建一个注解Annotation: MyConstraint
校验的时候,我们如果需要根据业务来校验,就可以使用这种方式,自定义了业务逻辑,可以从传入的值进行判断,减少了很多不必要的操作;
4.5 修改update
PutMapping("/{id:\\d+}")
public User update(@Valid @RequestBody User user,BindingResult errors){
if(errors.hasErrors()){
errors.getAllErrors().stream().forEach(err0r->{System.out.println(error.getDefaultMessage());});
}
}
4.6 修改-- 测试用例
@Test
public void whenUpdateSuccess() throws Exception{
Date date=new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
String content ="{\"username\":\"tom\",\"password\":null,\"birthday\":"+date.getTime}; //将时间戳信息给content
String result=mockMvc.perform(post("/user/1").contentType(MediaType.APPLICATION_JSON_UTF8)
.content(content))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value("1"))
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}
4.7 测试用例
@Test
public void whenDeleteSuccess() throws Exception{
mockMvc.perform(delete("/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk());
}
4.8 delete接口
@DeleteMapping("/{id:\\d+}")
public void delete(@PathVariable String id){
System.out.println(id);
}
五. 服务异常处理
5.1 概述:
- RESTful API错误处理机制
-
SpringBoot中默认的错误处理机制
- SpringBoot中的错误处理会进行判断是浏览器发出来的还是客户端发出来的,
- 如果是浏览器,会返回一个错误的HTML提示
- 如果是客户端,会返回一个错误的JSON
- 在BasicErrorController类它继承了AbstractErrorController,里面有两个方法,errorHtml和error, 前者是对浏览器的错误处理,后者是客户端的错误处理,当请求头中没有text/html时,就会返回json结果,如果有的话则返回HTML页面
- 在浏览器发送请求中我们可以看到:Accept:text/html 所以才会有这样的两种不同结果 而在客户端中一般为: Accept: / 两种请求方式进入的方法不一样,所以返回的结果也不同;
- SpringBoot中的错误处理会进行判断是浏览器发出来的还是客户端发出来的,
-
自定义异常处理
- 自定义HTML异常
- 自定义JSON格式异常
-
5.2 格式不正确的情况
- 当我们请求后端时,传入的值不正确,而后端又使用了@NotBlank,@Valid,BindingResult等方式的时候,SpringBoot会自动收集错误,然后将错误信息和错误状态码等融合,然后一起返回给前端;
- 图示:
[外链图片转存失败(img-XYGvw6Oe-1563862092758)(https://i.imgur.com/cMAe2PU.jpg)]
5.3 自定义HTML异常
- 位置:src/main/resources/resources/error/404.html
- 输入:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> </head> 您所访问的页面不存在 </body> </html>
- 当放在这个路径下,访问的为404的时候,会自动显示这个页面;
- 我们比如也可以定义500…等等
5.4 自定义JSON异常(这里可以包含一些其他的信息)
@ControllerAdvice //管辖所有的Controller类下的异常,本身不处理HTTP请求
public class ControllerExceptionHandler{
@ExceptionHandler(UserNotExistException.class) //加了这个注解后,其他Controller抛出的这个类型的异常就会转入到这个方法进行处理;0.
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) //500状态码
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; //返回错误后的Result
}
}
六. RESTful API的拦截
6.1 概述:
- 过滤器(Filter)
- 拦截器(Interceptor)
- 切片(Aspect)
6.2 自定义过滤器
- 创建类:src/main/java/filter/TimeFilter.java:
@Component public class TimeFilter implements Filter{ @Override public void 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); //这个表示用户执行的方法,因为过滤器有两部分,第一部分是用户请求前会进入一次过滤器,当用户执行完方法之后也会执行一次过滤器,而包裹我们的doFilter方法则是执行方法前,这里的chain.doFilter是执行方法时,当执行完后又会进入doFIlter里面,继续未完成的代码,通过这里我们可以实现计算一个方法的执行时间需要多久; 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"); } }
6.3 如何将第三方过滤器加入到我们的项目
- 概述: 在传统的SSM项目中我们可以使用web.xml将第三方的过滤器配置进去,而在SpringBoot没有配置文件不能以这种配置的方式,又不能改代码不能加入Bean,第三方过滤器的代码一般是不容易改的,如何做呢?
- 解决方案:
- 思路描述: 我们可以通过创建一个配置类,然后将写好的过滤器类new一个对象出来然后加入到注册Filter中,配置好路径,就可以使用啦~
- 步骤如下:
- 第三方的Filter是没有@Component注解的,我们可以直接使用上面的Filter代码,只需要将@Component注释掉就可以当做一个第三方的过滤器了;
- 新建一个配置类 src/main/java/web/config/WebConfig.java:
@Configuration public class WebConfig{ @Bean public FilterRegistrationBean timeFilter(){ FilterRegistrationBean registrationBean =new FilterRegistrationBean(); //创建一个注册过滤器 TimeFilter timeFilter=new TimeFilter(); //创建一个自定义过滤器对象 registrationBean.setFiter(timeFilter); //将自定义过滤器注册到注册过滤器中; List<String> urls=new ArrayList<>(); ursls.add("/*"); registrationBean.setUrlPatterns(urls); //给过滤器设置拦截范围 return registrationBean } ~~~
- 注意: 我们平时加了@Component注解的过滤器范围是所有的路径,而这里的自定义过滤器通过设置urls集合,可以自定义设置拦截路径;
6.4 拦截器
- 为什么要使用拦截器呢?
- 在过滤器Filter中,我们拦截的是Request请求,所以我们不知道具体执行的方法等,如果需要在这些方面进行我们可以使用拦截器(Interceptor),它是Spring提供的一套拦截机制,更贴合;
- 操作:新建一个类:src/main/java/web/filter/Interceptor/TimeIntercetor.java
- 它需要实现HandlerInterceptor并实现三个方法:preHandle(方法执行前,它是一个Boolean值,如果返回false,则不会执行postHandle方法),postHandle(方法执行后执行,如果方法抛出异常则不执行),afterCompletion(无论如何都会在方法之后且不管是否有异常都会执行,前面两个拦截器方法之后执行,类似于trycath中的finaly代码块)
- 拦截器如果创建好了,加了@Component注解也需要
- 代码:
- 自定义拦截器代码:TimeInterceptor.java:
@Component //这里只声明注解是不行的,我们必须添加到Interceptor中去 public TimeInterceptor implements HandlerInterceptor{ @Override public boolean preHandle(HttpServletRequest request ,HttpServletResponse response,Object handler) throws Exceptoon{ System.out.println(((HanderMethod)handler).getBean().getClass().getName()); //打印执行方法的类名 System.out.println(((HandleMethod)handler).getMethod().getName()); //打印执行方法的方法名 request.setAttribute("startTime",new Date().getTime()); return true; //这里只有返回ture才会执行后面的postHandle方法 } @Override public void postHandle(HttpServletRequest request ,HttpServletResponse response,Object handler,ModelAndView modelAndView ) throws Exception{ Long start=(Long) request.getAttribute("startTime"); //从上一个拦截器方法中找到这个start时间,然后通过执行方法后调用的这个PostHandle方法的时间相减,就可以得到方法的调用花费时间了; System.out.println("time interceptor 耗时:"+ (new Date().getTime()-start)) } @Override public void afterCompletion(HttpServletRequest request,HttpServletResponse response,Object handler,Exceptiopn ex) throws Exception{ System.out.println("ex is "+ex); //这里打印异常,如果异常被统一异常处理所捕捉了,那么即便发生了异常,这里打印还是会为null,需要注意; } }
- 在配置类中将拦截器加入;
@Configuration public class WebConfig extends WebMvcConfigurerAdpter{ @Autowired private TimeInterceptor timeInterceptor; @Override public void addInterceptors(InterceptorRegistry registry){ registry.addInterceptor(timeInterceptor); //通过引入已经加入@Compnonet注解的拦截器,然后将其挂载到Interceptor上,即可实现了自定义拦截器 } }
- 自定义拦截器代码:TimeInterceptor.java:
拦截器不仅能拦击我们的东西,也可以拦截Spring相关的Controller的方法;
拦截器虽然能处理方法,但是它不能知道方法中参数具体的类型,参数中具体的值,它的局限性我们可以通过切片解决;
6.5 切片Aspect
-
Spring AOP 简介:
- 切片它本身是一个类
- 切入点(注解)
- 在哪些方法上起作用
- 在什么时候起作用
- 增强(方法)
- 起作用时执行的业务逻辑
-
操作:
- 添加依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>Spring-boot-starter-aop</artifactId> </dependency>
- 创建切片类: src/main/java/web/aspect/TimeAspect.java:
@Aspect @Component public class TimeAspect{ @Around("execution(* com.migu.web.controller.UserController.*(...))") //这里是围绕,我们还可以使用@After,@Before等 public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable{ Object[] args= pjp.getArgs(); //获取到所有的参数 for(Object arg: args){ System.out.println("arg is "+arg); } long start =new Date().getTime(); Object object=pjp.proceed(); //拿到返回值 return object; } }
- 添加依赖:
6.6 拦截请求的顺序
- 图示,当用户请求的时候,先经过Filter->Controller,由外向内,当请求返回的时候,则由内向外,比如抛出异常的时候,如果Aspect切片有设置相关的,则它是最先捕获到的;如果不放出异常,则其他的捕获则会null
过滤器只能拿到原始的Request,而拦截器能拿到方法但是不能拿到具体的值,而切片则可以拿到具体的参数和具体的返回值等。
七. 文件的上传和下载
7.1 文件上传的测试用例
@Test
public void whenUploadSuccess() throws Exception{
String result=mockMvc.perform(fileUpload("/file")
.file(new MockMultipartFile("file","text.txt","multipart/form-data","hello upload".getBytes("UTF-8")))
.andExpect(status().isOk)
.andReturn().getResponse().getContentAsString();
System.out.println(result);
)
}
7.2 文件上传服务
@RestController
@RequestMapping("/file")
public class FileController{
@PostMapping
public FileInfo uplaod(MultipartFile file) throws Exception{
System.out.println(file.getName()+"-"+file.getOriginalFilename()+"-"+file.getSize());
String folder="/..." //这里是定义文件上传的地址,我们这里是存入本地,如果是上传到图片服务器之类的,则添加相应的东西即可;
File localFile=new File(folder,new Date().getTime()+".txt"); //将新文件改名
file.transferTo(localFile); //执行上传操作
return new FileInfo(localFile.getAbsolutePath());
}}
7.3 文件下载服务
- 添加依赖:
<dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.5</version> </dependency>
- 文件下载代码:
@GetMapping("/{id}") public void download(@Pathvaariable String id,HttpServletRequest request,HttpResponse response){ try( InputStream inputStream =new FileInputStream(new File(folder,id+".txt")); OutputStream outputStream =response.getOutpustStream(); ){ response.setContentType("application/x-download"); response.addHeader("Content-Disposition","attachment;filename=test.txt"); IOUtils.copy(inputStream,outputStream); }} ~~~
这里是提供一个id值,然后以这个id值命名将文件下载下来;先InputStream读取文件信息,然后再使用outputStream输出,通过工具类IOUtils.copy完成;在这里面我们将try()中声明了inputStream 和outStream 流,这是JDK7以上的新特性,我们就可以不必手动关闭流了;
八. 异步处理REST服务
8.1 概述:
- 使用Runnable异步处理Rest服务
- 由主线程调用副线程进行处理,性能上有损失
- 使用DeferredResult异步处理Rest服务
- 它是相当于Controller中应用一的两个线程是分开的,一个负责接收请求并将请求内容发送到消息队列中,而另外一个应用二则一直监听,当接收到处理请求后它就开始处理并将处理结果发送到消息队列;应用一的另外一个线程则监听到处理结果后将结果响应给前端;
- 这里面有三个职能划分,一个负责接收请求并转发到消息队列,一个负责监听请求并处理请求将结果给消息队列,一个负责接收处理结果并响应给前端; 这里面消息队列则是负责通信的核心;
- 异步处理配置
为什么要使用异步处理?在高并发环境下有较好的吞吐量,比同步处理能同时处理更多的请求;
注意:如果想处理异步的请求有上面两种方式,但是在拦截方面我们也需要一个@Configuration注解所修饰的类实现WebMavConfigurerAdapter并重写configureAsyncSupport方法,在重写方法中configurer.registerCollable…或configurer.registerDeferredResult…要注册这两个异步的,因为同步和异步的拦截是不一样的;
相关的东西涉及的方面很多,难度也较大,这里只大概提一下,有兴趣的朋友可以单独找这方面的了解一下
九. 与前端开发并行工作
9.1 概述:
- 使用swagger自动生成html文档
- 根据写的代码自动生成文档
- 使用WireMock快速伪造RESTful服务
9.2 操作:
- 添加依赖:
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <2.7.0> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <2.7.0> </dependency>
9.2 添加注解
- 在一个需要生成文档的类上添加注解: @EnableSwagger2
- 针对一个方法的描述注解,加在方法上面: @ApiOperation(value=“用户查询服务”)
- 针对参数的属性,比如针对一个User用户的具体属性描述,我们可以加这个注解在它的实体类的具体属性值上,如:
@ApiModelProperty(value="用户年龄起始值") private int age;
这几个是比较常用的,其他的注解需要了解的可以网上找相关文章;
9.3 伪造服务
-
我们后端如果做伪造服务,那么前端就可以不用造假数据了。因为如果前端造假数据的话,如果是APP(安卓,苹果),浏览器等众多前端组每个都自己造假数据的话,对工作效率有影响,同时质量也残差不齐,我们后端可以做这方面的事情;
-
WireMock:它是一个独立的服务器,可以定义当它收到了什么请求后,返回什么样的响应;
9.4 操作
- 下载WireMock
- 在下载位置上,通过命令启动:java -jar wiremock-standalone-2.7.1.jar --port 8062
- 添加依赖:
<dependency> <groupId>com.github.tomakehurst</groupId> <artifactId>wiremock</artifactId> <version>2.5.1</version> </dependency> //如果不要版本号可以省略
- 创建一个类MockServer.java:
public class MockServer{ public static void main(String[] args){ WireMock.configureFor(8062); //因为是在本机上测试,所以可以不用指定ip WireMock.removeAllMappings(); //每次开服务清除以前的配置,重新加配置更新; WireMock.stubFor(geturlPathEqualTo("/order/1")).willReturn(aResponse().withBody("{\"id\":1}").withStatus(200))); //这里的链式编程还可以继续加东西;而且相关的Json我们可以单独写,然后读取出来; }} //浏览器访问: localhost:8062/order/1
9.5 局部改造:
- 新建一个文件:src/main/resources/mock/response/01.txt:
{ "id": 1 "type": "A" }
- 对类进行改造:
public class MockServer{ public static void main(String[] args){ WireMock.configureFor(8062); //因为是在本机上测试,所以可以不用指定ip WireMock.removeAllMappings(); //每次开服务清除以前的配置,重新加配置更新; ClassPathResource resource=new ClassPathResource("mock/response/01.txt"); String content =StringUtils.join(FileUtils.readLines(resource.getFile(),"UTF-8").toArray(),"\n") WireMock.stubFor(geturlPathEqualTo("/order/1")).willReturn(aResponse().withBody("{\"id\":1}").withStatus(200))); //这里的链式编程还可以继续加东西;而且相关的Json我们可以单独写,然后读取出来; }} //浏览器访问: localhost:8062/order/1
WireMock可以做各种请求,以及其他很多的高级特性,与前端的联合是很方便的;如果有深入了解的朋友可以详细查看官方文档;