基于springboot创建RESTful风格接口

作者: 我可能是个假开发 | 来源:发表于2018-04-27 14:00 被阅读390次

    基于springboot创建RESTful风格接口

    RESTful API风格

    restfulAPI.png

    特点:

    1. URL描述资源
    2. 使用HTTP方法描述行为。使用HTTP状态码来表示不同的结果
    3. 使用json交互数据
    4. RESTful只是一种风格,并不是强制的标准
    REST成熟度模型.png

    一、查询请求

    1.编写单元测试

    @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 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输出内容
    

    查询请求:

    @RestController
    public 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)
    @SpringBootTest
    public 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;
            }
    

    当希望对传递进来的参数作一些限制时,可以使用正则表达式:

    //测试提交错误信息
    @Test
    public 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;
    }
    

    单元测试:

    //查询
    @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}]
    }
    
    
    //获取用户详情
    @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}
        
    }
    

    代码重构:

    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才可以接收前端传递过来的参数

    @PostMapping
    public 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:

    @PostMapping
    public 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

    ①在实体中添加相应验证注解:

    @NotBlank
    private String password;
    

    ②后台接收参数时加@Valid注解

    @PostMapping
    public 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:校验成功
        }
        
    }
    

    五、处理删除

    单元测试:

    @Test
    public 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

    相关文章

      网友评论

      • 知识学者:springboot, 据说会很不错,:grin: 比ssh简单。

      本文标题:基于springboot创建RESTful风格接口

      本文链接:https://www.haomeiwen.com/subject/lgvblftx.html