美文网首页技术我爱编程我的Spring MVC
SpringBoot快速开发Restful Api

SpringBoot快速开发Restful Api

作者: 歪歪歪比巴卜 | 来源:发表于2018-03-31 11:21 被阅读441次

    Spring-Boot Restful Api

    1、Restful API开发

    1.1 Restful简介

    springMVC对编写Restful Api提供了很好的支持。

    Restful Api有三个主要的特性:

    • 是基于Http协议的,是无状态的。
    • 是以资源为导向的
    • 人性化的,返回体内部包含相关必要的指导和链接

    面向资源?
    传统的Api接口以动作为导向,并且请求方法单一。例如/user/query?id=1 GET方法 ;/user/create
    POST方法 而在resultful风格下以资源为导向,例如: /user/id(GET方法,获取) /user/(POST方法,创建)

    restful api 用url描述资源,用Http方法描述行为,用Http状态码描述不同的结果,使用json作为交互数据(包括入参和响应)
    restful只是一种风格并不是一种强制的标准

    1.2 编写restful api 测试用例

    因为restful api 与传统api存在一些风格上的差异,例如以method代表行为。所以在开发的过程中需要一边开发一边测试,测试我们的接口是否达到了预期的目的。springBoot提供了开发restful api测试用例的方法。首先导入依赖

        <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    

    1.3 编写restful接口

    1.3.1 基本注解

    • @RestController 声明一个controller负责提供restful接口
    • @RequestMapping 将请求的url映射到方法
    • @RequetParam 映射请求参数到方法请求参数 可以指定required指定此参数是否必填,name参数指定别名,defaultValue指定默认值。在传参时,SpringMVC会自动封装参数,所以可以在方法中用一个对象参数接收

    1.3.2 @PathVariable

    映射url片段到java方法参数

        @GetMapping("/user/{id}")
        public User getUserInfo(@PathVariable("id") String id){
            return new User("sico","12345");
        }
    

    1.3.3 在url声明中使用正则表达式

    在@pathVariable中url片段默认可以接收任何格式,任何类型,可以用正则表达式加以限定,例如:

        /**
         * 获取用户详情,利用正则表达式限定为只接收数字
         * @param id
         * @return
         */
        @GetMapping("/user/{id:\\d+}")
        public User getUserInfo(@PathVariable("id") String id){
            return new User("sico","12345");
        }
    
    

    1.3.4 使用@jsonView控制json输出内容

    SpringMVC会将实体对象转换成json返回。有时候我们希望在不同的请求中隐藏一些字段。可以用@JsonView控制输出内容。
    使用@jsonView注解有以下步骤:

    • 使用接口来声明多个视图
    • 在值对象的getter方法上指定视图
    • 在controller方法上指定视图

    使用接口声明视图
    此接口只作声明使用,可以直接放置到目标实体内部,示例:

    public class User implements Serializable{
    
        public interface SimpleView{};
        public interface DetailView extends SimpleView{};
        //....
    }
    

    注意继承关系,DetailView继承了SimpleView。即视图DetailView会显示被SimpleView标注的视图

    在值对象上的getter方法上指定视图

        @JsonView(SimpleView.class)
        public String getUsername() {
            return username;
        }
        //...
        @JsonView(DetailView.class)
        public String getPassword() {
            return password;
        }
    

    在方法上指定视图

        /**
         * 获取用户详情,利用正则表达式限定为只接收数字
         * @param id
         * @return
         */
        @GetMapping("/user/{id:\\d+}")
        @JsonView(User.DetailView.class)
        public User getUserInfo(@PathVariable("id") String id){
            return new User("sico","12345");
        }
    

    由于视图的继承关系,DetailView任然会显示被SimpleView标注的字段

    1.3.5 RequestMapping的变体

    RequestMapping有以下变体,他们分别对应了不同的请求方法

    • @GetMapping 对应GET方法
    • @PostMapping 对应POST方法
    • @PutMapping 对应PUT方法
    • @DeleteMapping 对应DELETE方法

    1.3.5 @RequestBody将请求体映射到java方法参数

    @(spring)RequestBody将请求中的请求体中的实体数据转换成实体对象,常用语PUT和POST

        /**
         * 创建用户
         * 仅有加入@RequestBody注解才能解析出请求体重传入的实体数据
         */
        @PutMapping("/user")
        public void create(@RequestBody User user){
            User user1=new User("cocoa","123",1);
        }
    

    1.3.6 @Valid注解和BindingResult验证请求参数的合法性并处理校验结果

    一般需要在请求接口中校验请求参数,例如参数是否为空,是否唯一等。

    • @NotBlack 非空注解,将此注解加到实体类属性上。
        @NotBlank
        private String username;
    

    在请求方法的字段上加上@valid注解时,以上的注解将生效。如果请求接口的参数无法通过校验,将返回400

        @PutMapping("/user")
        public void create(@Valid @RequestBody User user){
            User user1=new User("cocoa","123",1);
        }
    

    BindingResult
    如果使用@valid注解,当参数不符合标准时。会直接返回400。而不会进入接口方法的方法体。如果需要对没通过校验的请求作一些处理。在使用BindingResult的情况下,如果用户传入的参数不符合约束。则相应的错误信息将会被放置在BindingRsult对象中。从BindingResult对象中取出错误信息:

        @PutMapping("/user")
        public void create(@Valid @RequestBody User user, BindingResult errors){
            if (errors.hasErrors()){
                errors.getAllErrors().stream().forEach(error->logger.error(error.getDefaultMessage()));
            }
            User user1=new User("cocoa","123",1);
        }
    

    如果没有通过非空校验,将包含错误。默认的非空错误信息是:"may not be null",这个错误信息可以自定义。

    1.3.6.1 hibernate validate常用校验注解
    image
    image
    1.3.6.2 获取校验错误信息(包括字段信息)

    使用fieldError可以获取错误的字段信息和错误信息

        @PutMapping("/user")
        public void update(@Valid @RequestBody User user,BindingResult errors){
            if (errors.hasErrors()){
                errors.getAllErrors().stream().forEach(error->{
                    FieldError fieldError=(FieldError) error;
                    String errorMessage=fieldError.getField()+" "+fieldError.getDefaultMessage();
                    logger.error(errorMessage);
                });
            }
            User user1=new User("cocoa","123",1);
        }
    
    1.3.6.3 自定义校验失败信息

    用以上方式虽然能够获得错误字段和错误信息,但过于麻烦。可以在校验注解中指定message值自定义错误信息。如下:

        @NotBlank(message = "用户名不能为空")
        private String username;
    
    1.3.6.3 自定义校验逻辑

    默认的校验注解能够满足大部分的校验要求,但是依然不能完全满足要求。例如需要校验一个字段是否唯一,就无法通过默认的注解完成。此时需要自定义校验逻辑。可以通过自定义的注解来实现和自定义校验器实现

    自定义注解

    @Target({ElementType.FIELD,ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    //java标准校验注解,validateBy指定校验的类
    @Constraint(validatedBy = NameUniqueValidator.class )
    public @interface NameUnique {
    
        //校验注解中必须实现以下三个属性
        String message() default "";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default { };
    }
    

    自定义校验器

    
    /**
     * 实现ConstraintValidator接口,第一个泛型指定用于标注验证的注解,第二个泛型指定倍标注值得类型
     * 不需要用@component等注解将验证类加入容器。spring会自动将此类加入容器
     */
    public class NameUniqueValidator implements ConstraintValidator<NameUnique,Object>{
    
        //在这个类中可以使用Spring @Autowire注解注入任何需要的对象
    
        /**
         * 校验器初始化
         * @param nameUnique
         */
        @Override
        public void initialize(NameUnique nameUnique) {
    
        }
    
        /**
         * 校验方法
         * @param o 待校验的值
         * @param constraintValidatorContext
         * @return 返回true代表校验成功,false代表校验失败
         */
        @Override
        public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
            //TODO 执行校验逻辑
            return false;
        }
    }
    

    1.4 服务异常处理

    1.4.1 SpringBoot默认的错误处理机制

    SpringBoot会自动的处理一些异常。例如访问了一个不存在的页面,当使用浏览器访问时,SpringBoot会返回一个默认的错误页面,如下所示:


    image

    但使用postman访问时,返回如下错误信息:

    {
        "timestamp": 1509626392183,
        "status": 404,
        "error": "Not Found",
        "message": "No message available",
        "path": "/12ss"
    }
    

    原理:SpringBoot中包含一个BasicErrorController类用于处理错误,处理/error请求。当它检测到请求头中包含text/html的时候,返回一个错误的页面。当没有这个请求头时,返回json格式的错误。如何判断请求是否来自网页?使用注解:@RequestMapping(produces="text/html")
    如下:

    
        @RequestMapping(
            produces = {"text/html"}
        )
        public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
            HttpStatus status = this.getStatus(request);
            Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
            response.setStatus(status.value());
            ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
            return modelAndView == null ? new ModelAndView("error", model) : modelAndView;
        }
    
        @RequestMapping
        @ResponseBody
        public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
            Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
            HttpStatus status = this.getStatus(request);
            return new ResponseEntity(body, status);
        }
    

    可以模仿这种处理机制,在同一个url下做出不同的响应。

    1.4.2 自定义异常处理

    1.4.2 自定义返回的浏览器错误页面

    自定义返回的浏览器错误页面只需要把相应的html文件放置在resources/resources/error文件夹下即可,404即404.html 500即500.html

    1.4.3 自定义返回的json格式的错误信息

    如果抛出自定义的异常,SpringBoot默认处理如下所示:

    {
        "timestamp": 1509629240633,
        "status": 500,
        "error": "Internal Server Error",
        "exception": "com.sicosola.security.demo.exception.ServiceException",
        "message": "用户不存在",
        "path": "/user/1"
    }
    

    自定义异常返回格式
    可以创建一个全局的控制器的错误处理器,从控制器抛出的异常都会在此处被拦截。可以在此处对它进行处理,首先自定义一个异常:

    
    public class ServiceException extends RuntimeException{
    
        private Integer code;
    
        private String desc;
    
        public ServiceException(Integer code, String desc) {
            super(desc);
            this.code = code;
            this.desc = desc;
        }
    
        public Integer getCode() {
            return code;
        }
    
        public void setCode(Integer code) {
            this.code = code;
        }
    
        public String getDesc() {
            return desc;
        }
    
        public void setDesc(String desc) {
            this.desc = desc;
        }
    }
    

    定义一个controller全局异常处理器,处理异常

    /**
     * 控制器错误处理器,从控制器抛出的异常被它拦截。
     * 可以在此处封装错误信息,以友好的方式返回给前端
     */
    
    @ControllerAdvice
    public class ControllerExceptionHandler {
    
        /**
         * 处理ServiceException
         * @return
         */
        @ExceptionHandler(ServiceException.class)
        @ResponseBody
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
        public Map<String,Object> HandlerServiceException(ServiceException e){
            Map<String,Object> errorMessage=new HashMap<>();
            errorMessage.put("code",e.getCode());
            errorMessage.put("desc",e.getDesc());
            return errorMessage;
        }
    
    }
    

    1.5 Restful api拦截

    一般来说,可以使用以下机制来拦截

    • 过滤器 (Filter)
    • 拦截器 (Interceptor)
    • 切片 (Aspect)

    1.5.1 使用Filter

    使用Filter仅需实现一个filter,将其加入容器即可。
    在SpringBoot中,如何将不可更改源码的第三方Filter加入Spring容器中?
    可以利用在配置类中利用FilterRegistrationBean将第三方过滤器注册到Spring

        /*
        用以下方式将第三方容器注册到Spring
         */
        @Bean
        public FilterRegistrationBean timeFilter(){
            FilterRegistrationBean registrationBean=new FilterRegistrationBean();
            //假设这是第三方容器
            TimeFilter filter=new TimeFilter();
            registrationBean.setFilter(filter);
            //可以声明这个filter在哪些路径起作用
            List<String> urls=new ArrayList<>();
            urls.add("/*");
            registrationBean.setUrlPatterns(urls);
            return registrationBean;
        }
    

    使用Filter的缺陷
    filter是由JavaEE提供的功能,它只能获取Http请求和Http响应的信息。无法知晓具体的业务是由某个控制器和某个方法完成的。

    1.5.2 拦截器

    拦截器是由Spring框架提供的功能,可以弥补Filter的不足

    自定义Interceptor实现HandlerInterceptor.实现其处理方法后,在configuration中配置。
    需要使配置类继承WebMvcConfigurerAdapter并覆盖其addInterceptors方法。

    1.5.2.1 实现一个拦截器
        /**
     * 记录服务调用时间的拦截器
     */
    @Component
    public class TimeInterceptor implements HandlerInterceptor{
    
        private Logger logger= LoggerFactory.getLogger(getClass());
    
        /**
         * 处理前
         * @param httpServletRequest
         * @param httpServletResponse
         * @param handler 此参数记录了处理对象,包括类名和方法名等信息
         * @return
         * @throws Exception
         */
        @Override
        public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception {
            //设置开始时间
            httpServletRequest.setAttribute("startTime",new Date().getTime());
            //获取当前拦截接口处理类(Controller)
            logger.error(((HandlerMethod)handler).getBean().getClass().getName());
            //获取当前拦截接口的处理方法
            logger.error(((HandlerMethod)handler).getMethod().getName());
            //只有返回true才会执行后面的方法
            return true;
        }
    
        /**
         * 接口成功返回后,如果调用控制器方法时控制器方法抛出异常。则post方法不会被调用
         * @param httpServletRequest
         * @param httpServletResponse
         * @param o
         * @param modelAndView
         * @throws Exception
         */
        @Override
        public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
            long startTime= (long) httpServletRequest.getAttribute("startTime");
            logger.error("TimeInterceptor耗时:"+(new Date().getTime()-startTime));
        }
    
        /**
         * 处理完成,无论控制器方法成功与否。都会进入这个方法
         * @param httpServletRequest
         * @param httpServletResponse
         * @param o
         * @param e
         * @throws Exception,当控制器方法抛出异常时,此exception有值,如果有全局异常处理器(参考ControllerExceptionHandler)它将拿不到异常对象
         */
        @Override
        public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
            long startTime= (long) httpServletRequest.getAttribute("startTime");
            logger.error("TimeInterceptor耗时:"+(new Date().getTime()-startTime));
        }
    }
    
    1.5.2.2 将拦截器注册到Spring
    @Configuration
    public class WebConfiguration extends WebMvcConfigurerAdapter{
    
    
        @Autowired
        TimeInterceptor timeInterceptor;
    
        /**
         * 此类继承自 WebMvcConfigureAdapter
          * @param registry 拦截器注册器
         */
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            //将timeInterceptor注册
            registry.addInterceptor(timeInterceptor);
        }
    }    
    

    1.5.3 切面

    拦截器能拦截请求并且能够获取到处理请求的控制器与方法。但是它依然无法拿到参数中的值。如果想获取参数的值,就需要使用切面。切片是Spring框架的核心功能之一。

    要使用AOP,首先需要定义一个切面(切面中定义了处理的逻辑),此处声明一个切片名为TimeAspect。在声明一个切入点(切入点约定切片在哪些方法上起作用,在什么时候上起作用)
    切入点常用的注解(约定在什么时候起作用)

    • @before 此注解约定切入点在目标方法执行前执行
    • @after 此注解约定切入点在目标方法执行后执行
    • @afterthrow 在方法抛出异常时调用
    • @around 覆盖了前3种,常用

    在什么方法上起作用
    在什么方法上起作用是用一个表达式指定的。

        //此注解声明类为切面
    @Aspect
    @Component
    public class TimeAspect {
    
        private Logger logger= LoggerFactory.getLogger(getClass());
    
        /**
         * 定义切入点,
         * 第一个*表示任何返回值,第二个表示任何方法最后表示任何参数
         * @param joinPoint 此对象中包含了被切入方法的信息
         * @return
         */
        @Around("execution(* com.sicosola.security.demo.web.controller.UserController.*(..))")
        public Object handlerControllerMethod(ProceedingJoinPoint joinPoint) throws Throwable {
            logger.error("Time aspect start !");
            //获取被切入方法的参数
            Object[] args = joinPoint.getArgs();
            for (Object arg:args){
                logger.error("args is "+arg);
            }
            long startTime=new Date().getTime();
            //执行目标方法,返回目标方法的返回值
            Object o = joinPoint.proceed();
    
            logger.error("耗时:"+(new Date().getTime()-startTime));
            return o;
        }
    }
    

    1.6 异步处理Rest服务

    使用异步处理服务可以提高服务器的吞吐量,并且这种异步的处理对客户端是透明的。
    在传统的同步模式下,所有的请求都在主线程中完成。Tomcat管理的线程是有最大数量的,当达到最大数量时。其它的请求就需要等待。而异步线程使用副线程,当请求发送到主线程时。主线程将任务交给副线程,主线程又可以继续接收请求。

    1.6.1 使用Runable异步处理Rest服务

    使用Callable单开一个线程执行任务,Callable是由java并发包提供的机制。

        @RequestMapping("/order")
        public Callable<String> Order() throws InterruptedException {
            logger.info("主线程开始");
            //使用Callable单开一个线程处理
            Callable<String> result=new Callable<String>() {
                @Override
                public String call() throws Exception {
                    logger.info("处理线程开始");
                    Thread.sleep(1000);
                    logger.info("处理线程结束");
                    return "success";
    
                }
            };
    
            Thread.sleep(1000);
            logger.info("主线程返回");
            return result;
        }
    

    可以看到如下的日志:

    2017-11-03 09:56:39.592  INFO 5148 --- [nio-8080-exec-1] c.s.s.demo.web.async.AsyncController     : 主线程开始
    2017-11-03 09:56:40.592  INFO 5148 --- [nio-8080-exec-1] c.s.s.demo.web.async.AsyncController     : 主线程返回
    2017-11-03 09:56:40.603  INFO 5148 --- [      MvcAsync1] c.s.s.demo.web.async.AsyncController     : 处理线程开始
    2017-11-03 09:56:41.603  INFO 5148 --- [      MvcAsync1] c.s.s.demo.web.async.AsyncController     : 处理线程结束
    

    可以看到处理业务实在副线程MvcAsync中打印出来的。根据日志可以看出,主线程几乎没有任何停顿就立即返回。

    1.6.2 使用DeffrredResult异步处理Rest服务。

    Runable并不能满足所有的场景,有时候可能使用消息队列在不同的服务器之间完成异步。使用Runable机制就不会有明显的效果。如下


    image

    此时需要使用DeffrredResult处理。它可以在两个不同的线程之间来传递。其大致处理流程如下:

    • 创建一个DeferredResultHolder
        @Component
    public class DeferredResultHolder {
    
        //key代表订单号,value代表处理结果
        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;
        }
    }
    
    
    • 控制器方法接收到请求发送到消息队列,并创建一个DiferedResult,以订单号为key,result为value放到holder的map中。
    • 处理成功的消息监听器在收到处理结果后从holder中取出对应的DiferedResult并设置值。一旦该result被设置值就会异步返回。

    最需要理解的是Holder,Holder只是作为一个容器保存了待接受值的所有diferredResult对象。Holder就作为两个不同线程之间的通信桥梁

    1.6.3 异步处理配置

    SpringWebMvcConfig中有个configureAsyncSupport方法,可以用此方法进行异步配置。可以在此配置类中注册异步拦截器,设置异步请求默认超时时间。设置自定义线程池。

        /**
         * 配置异步处理
         * @param configurer
         */
        @Override
        public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
            //注册异步拦截器,此拦截器
            //configurer.registerCallableInterceptors();
            //configurer.registerDeferredResultInterceptors();
            //设置异步请求的默认超时时间
            configurer.setDefaultTimeout(10);
            //自定义线程池替代Spring默认的线程池
           // configurer.setTaskExecutor();
        }
    

    2 SpringBoot中的配置信息封装

    Spring Boot中一般会在resources目录下使用.properties文件或者.yml文件进行一些系统的配置。我们可以自定义自己的配置逻辑。自定义配置并在系统中读取配置。

    首先利用@ConfigurationProperties(prefix="---")声明配置类,其中prefix是配置前缀。
    然后利用@EnableConfigurationProperties使配置类起作用。参考:

    public class BrowserProperties {
    
        private String loginPage;
    
        public String getLoginPage() {
            return loginPage;
        }
    
        public void setLoginPage(String loginPage) {
            this.loginPage = loginPage;
        }
    }
    
    
    /**
     * sico-security框架配置积累
     */
    
    @ConfigurationProperties(prefix = "sico.security")
    public class SecurityProperties {
    
        private BrowserProperties browser=new BrowserProperties();
    
        public BrowserProperties getBrowser() {
            return browser;
        }
    
        public void setBrowser(BrowserProperties browser) {
            this.browser = browser;
        }
    }
    
    @Configuration
    //SecurityProperties配置读取器生效
    @EnableConfigurationProperties(SecurityProperties.class)
    public class SecurityCoreConfig {
    
    }
    

    需要特别注意的是配置类中的属性名必须和配置项的名称完全相同,否则将无法正常读取

    相关文章

      网友评论

        本文标题:SpringBoot快速开发Restful Api

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