基于springboot创建RESTful风格接口

基于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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,736评论 6 342
  • 这是我在高中就很喜欢一则故事。 有一个男孩有着很坏的脾气,一天他的父亲给了他一袋钉子,告诉他,每当他发脾气的时候就...
    Louise718阅读 226评论 0 2
  • 就想说出来。跟她说,能不能别一整天的都跟我讲你男朋友,什么都分享,任何细节都不放过。甚至你们的吻也可以细节化出来,...
    张小橘阅读 221评论 0 2
  • 文/左岸江南 20岁到30岁,是女人一生中最美的时候,也是职场发展的黄金期。这个时候,好看是一种天赋资源,是资本,...
    左岸江南阅读 505评论 0 1