RESTful API风格
restfulAPI.png
特点:
URL描述资源
使用HTTP方法描述行为。使用HTTP状态码来表示不同的结果
使用json交互数据
RESTful只是一种风格,并不是强制的标准
REST成熟度模型.png
一、查询请求
1.编写单元测试
@RunWith(SpringRunner.class)@SpringBootTestpublic class UserControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void setup() { mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); } //查询 @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("sort", "age,desc") .contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(status().isOk()) .andExpect(jsonPath("$.length()").value(3)) .andReturn().getResponse().getContentAsString();//将服务器返回的json字符串当成变量返回 System.out.println(result);//[{"username":null},{"username":null},{"username":null}] } }
2.使用注解声明RestfulAPI
常用注解
@RestController 标明此Controller提供RestAPI@RequestMapping及其变体。映射http请求url到java方法@RequestParam 映射请求参数到java方法的参数@PageableDefault 指定分页参数默认值@PathVariable 映射url片段到java方法的参数 在url声明中使用正则表达式@JsonView控制json输出内容
查询请求:
@RestControllerpublic class UserController { @RequestMapping(value="/user",method=RequestMethod.GET) public List<User> query(@RequestParam(name="username",required=false,defaultValue="tom") String username){ System.out.println(username); List<User> users = new ArrayList<>(); users.add(new User()); users.add(new User()); users.add(new User()); return users; } }
①当前端传递的参数和后台自己定义的参数不一致时,可以使用name属性来标记:
(@RequestParam(name="username",required=false,defaultValue="hcx") String nickname
②前端不传参数时,使用默认值 defaultValue="hcx"
③当查询参数很多时,可以使用对象接收
④使用Pageable作为参数接收,前台可以传递分页相关参数
pageSize,pageNumber,sort;
也可以使用@PageableDefault指定默认的参数值。
@PageableDefault(page=2,size=17,sort="username,asc")//查询第二页,查询17条,按照用户名升序排列
3.jsonPath表达式书写
github链接:https://github.com/json-path/JsonPath
jsonpath表达式.png
打包发布maven项目.png
二、编写用户详情服务
@PathVariable 映射url片段到java方法的参数
在url声明中使用正则表达式
@JsonView控制json输出内容
单元测试:
@RunWith(SpringRunner.class)@SpringBootTestpublic class UserControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void setup() { mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); } //获取用户详情 @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); //{"username":"tom","password":null} }
后台代码:
@RestController@RequestMapping("/user")//在类上声明了/user,在方法中就可以省略了public class UserController { @RequestMapping(value="/user/{id}",method=RequestMethod.GET) public User getInfo(@PathVariable String id) { User user = new User(); user.setUsername("tom"); return user; }
当希望对传递进来的参数作一些限制时,可以使用正则表达式:
//测试提交错误信息@Testpublic void whenGetInfoFail() throws Exception { mockMvc.perform(get("/user/a") .contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(status().is4xxClientError()); }
后台代码:
@RequestMapping(value="/user/{id:\\d+}",method=RequestMethod.GET)//如果希望对传递进来的参数作一些限制,使用正则表达式@JsonView(User.UserDetailView.class)public User getInfo(@PathVariable String id) { User user = new User(); user.setUsername("tom"); return user; }
使用@JsonView控制json输出内容
1.场景:在以上两个方法中,查询集合和查询用户详细信息时,期望查询用户集合时不返回密码给前端,而在查询单个用户信息时才返回。
2.使用步骤:
①使用接口来声明多个视图
②在值对象的get方法上指定视图
③在Controller方法上指定视图
在user实体中操作:
package com.hcx.web.dto;import java.util.Date;import javax.validation.constraints.Past;import org.hibernate.validator.constraints.NotBlank;import com.fasterxml.jackson.annotation.JsonView;import com.hcx.validator.MyConstraint;public class User { public interface UserSimpleView{}; //有了该继承关系,在显示detail视图的时候同时会把simple视图的所有字段也显示出来 public interface UserDetailView extends UserSimpleView{}; @MyConstraint(message="这是一个测试") private String username; @NotBlank(message = "密码不能为空") private String password; private String id; @Past(message="生日必须是过去的时间") private Date birthday; @JsonView(UserSimpleView.class) public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; } @JsonView(UserSimpleView.class) public String getId() { return id; } public void setId(String id) { this.id = id; } @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; } }
在具体的方法中操作Controller:
@GetMapping@JsonView(User.UserSimpleView.class)public List<User> query(@RequestParam(name="username",required=false,defaultValue="tom") String username){ System.out.println(username); List<User> users = new ArrayList<>(); users.add(new User()); users.add(new User()); users.add(new User()); return users; }@RequestMapping(value="/user/{id:\\d+}",method=RequestMethod.GET)//如果希望对传递进来的参数作一些限制,就需要使用正则表达式@JsonView(User.UserDetailView.class)public User getInfo(@PathVariable String id) { User user = new User(); user.setUsername("tom"); return user; }
单元测试:
//查询@Testpublic 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("sort", "age,desc") .contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(status().isOk()) .andExpect(jsonPath("$.length()").value(3)) .andReturn().getResponse().getContentAsString();//将服务器返回的json字符串当成变量返回 System.out.println(result);//[{"username":null},{"username":null},{"username":null}]}//获取用户详情@Testpublic 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); //{"username":"tom","password":null} }
代码重构:
1.@RequestMapping(value="/user",method=RequestMethod.GET)
替换成:
@GetMapping("/user")
2.在每个url中都重复声明了/user,此时就可以提到类中声明
@RestController@RequestMapping("/user")//在类上声明了/user,在方法中就可以省略了public class UserController { @GetMapping @JsonView(User.UserSimpleView.class) public List<User> query(@RequestParam(name="username",required=false,defaultValue="tom") String username){ System.out.println(username); 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(@PathVariable String id) { User user = new User(); user.setUsername("tom"); return user; } }
三、处理创建请求
1.@RequestBody 映射请求体到java方法的参数
单元测试:
@Test public void whenCreateSuccess() throws Exception { Date date = new Date(); System.out.println(date.getTime());//1524741370816 String content = "{\"username\":\"tom\",\"password\":null,\"birthday\":"+date.getTime()+"}"; String result = 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(result);//{"username":"tom","password":null,"id":"1","birthday":1524741229875} }
Controller:要使用@RequestBody才可以接收前端传递过来的参数
@PostMappingpublic User create(@RequestBody User user) { System.out.println(user.getId()); //null System.out.println(user.getUsername()); //tom System.out.println(user.getPassword());//null user.setId("1"); return user; }
2.日期类型参数的处理
对于日期的处理应该交给前端或app端,所以统一使用时间戳
前端或app端拿到时间戳,由他们自己决定转换成什么格式,而不是由后端转好直接给前端。
前端传递给后台直接传时间戳:
@Test public void whenCreateSuccess() throws Exception { Date date = new Date(); System.out.println(date.getTime());//1524741370816 String content = "{\"username\":\"tom\",\"password\":null,\"birthday\":"+date.getTime()+"}"; String result = 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(result);//{"username":"tom","password":null,"id":"1","birthday":1524741229875}(后台返回的时间戳) }
Controller:
@PostMappingpublic User create(@RequestBody User user) { System.out.println(user.getId()); //null System.out.println(user.getUsername()); //tom System.out.println(user.getPassword());//null System.out.println(user.getBirthday());//Thu Apr 26 19:13:49 CST 2018(Date类型) user.setId("1"); return user; }
3.@Valid注解和BindingResult验证请求参数的合法性并处理校验结果
1.hibernate.validator中的常用验证注解:
注解及其含义.png
①在实体中添加相应验证注解:
@NotBlankprivate String password;
②后台接收参数时加@Valid注解
@PostMappingpublic User create(@Valid @RequestBody User user) { System.out.println(user.getId()); //null System.out.println(user.getUsername()); //tom System.out.println(user.getPassword());//null System.out.println(user.getBirthday());//Thu Apr 26 19:13:49 CST 2018 user.setId("1"); return user; }
2.BindingResult:带着错误信息进入方法体
@PostMapping public User create(@Valid @RequestBody User user,BindingResult errors) { if(errors.hasErrors()) { //有错误返回true errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage())); //may not be empty } System.out.println(user.getId()); //null System.out.println(user.getUsername()); //tom System.out.println(user.getPassword());//null System.out.println(user.getBirthday());//Thu Apr 26 19:13:49 CST 2018 user.setId("1"); return user; }
四、处理用户信息修改
1.自定义消息
@Test public void whenUpdateSuccess() throws Exception { //一年之后的时间 Date date = new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); System.out.println(date.getTime());//1524741370816 String content = "{\"id\":\"1\",\"username\":\"tom\",\"password\":null,\"birthday\":"+date.getTime()+"}"; String result = 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(result);//{"username":"tom","password":null,"id":"1","birthday":1524741229875} }
Controller:
@PutMapping("/{id:\\d+}")public User update(@Valid @RequestBody User user,BindingResult errors) { /*if(errors.hasErrors()) { //有错误返回true errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage())); //may not be empty }*/ if(errors.hasErrors()) { errors.getAllErrors().stream().forEach(error -> { //FieldError fieldError = (FieldError)error; //String message = fieldError.getField()+" "+error.getDefaultMessage(); System.out.println(error.getDefaultMessage()); //密码不能为空 //生日必须是过去的时间 //birthday must be in the past //password may not be empty } ); } System.out.println(user.getId()); //null System.out.println(user.getUsername()); //tom System.out.println(user.getPassword());//null System.out.println(user.getBirthday());//Thu Apr 26 19:13:49 CST 2018 user.setId("1"); return user; }
实体:
public class User { public interface UserSimpleView{}; //有了该继承关系,在显示detail视图的时候同时会把simple视图的所有字段也显示出来 public interface UserDetailView extends UserSimpleView{}; @MyConstraint(message="这是一个测试") private String username; @NotBlank(message = "密码不能为空") private String password; private String id; @Past(message="生日必须是过去的时间") private Date birthday; @JsonView(UserSimpleView.class) public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; } @JsonView(UserSimpleView.class) public String getId() { return id; } public void setId(String id) { this.id = id; } @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; } }
2.自定义校验注解
创建一个注解MyConstraint:
@Target({ElementType.METHOD,ElementType.FIELD})//可以标注在方法和字段上@Retention(RetentionPolicy.RUNTIME)//运行时注解@Constraint(validatedBy = MyConstraintValidator.class)//validatedBy :当前的注解需要使用什么类去校验,即校验逻辑public @interface MyConstraint { String message();//校验不通过要发送的信息 Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; }
校验类:MyConstraintValidator:
public class MyConstraintValidator implements ConstraintValidator<MyConstraint, Object> { /*ConstraintValidator<A, T> 参数一:验证的注解 参数二:验证的类型 ConstraintValidator<MyConstraint, String> 当前注解只能放在String类型字段上才会起作用 */ @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 false;//false:校验失败;true:校验成功 } }
五、处理删除
单元测试:
@Testpublic void whenDeleteSuccess() throws Exception { mockMvc.perform(delete("/user/1") .contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(status().isOk()); }
Controller:
@DeleteMapping("/{id:\\d+}")public void delete(@PathVariable String id) { System.out.println(id); }
六、RESTful API错误处理
1.Spring Boot中默认的错误处理机制
Spring Boot中默认的错误处理机制,
对于浏览器是响应一个html错误页面,
对于app是返回错误状态码和一段json字符串
2.自定义异常处理
①针对浏览器发出的请求
在src/mian/resources文件夹下创建文件夹error编写错误页面
错误页面.png
对应的错误状态码就会去到对应的页面
只会对浏览器发出的请求有作用,对app发出的请求,错误返回仍然是错误码和json字符串
②针对客户端app发出的请求
自定义异常:
package com.hcx.exception;public class UserNotExistException extends RuntimeException{ private static final long serialVersionUID = -6112780192479692859L; private String id; public String getId() { return id; } public void setId(String id) { this.id = id; } public UserNotExistException(String id) { super("user not exist"); this.id = id; } }
在Controller中抛自己定义的异常
//发生异常时,抛自己自定义的异常@GetMapping("/{id:\\d+}")@JsonView(User.UserDetailView.class)public User getInfo1(@PathVariable String id) { throw new UserNotExistException(id); }
默认情况下,springboot不读取id的信息
抛出异常时,进入该方法进行处理:
ControllerExceptionHandler:
@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; } }
完整Demo链接:https://github.com/GitHongcx/RESTfulAPIDemo
作者:JS_HCX
链接:https://www.jianshu.com/p/733d788ea94d