技术我爱编程我的Spring MVC

SpringBoot快速开发Restful Api

2018-03-31  本文已影响441人  歪歪歪比巴卜

Spring-Boot Restful Api

1、Restful API开发

1.1 Restful简介

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

Restful Api有三个主要的特性:

面向资源?
传统的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 基本注解

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注解有以下步骤:

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

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有以下变体,他们分别对应了不同的请求方法

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验证请求参数的合法性并处理校验结果

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

    @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拦截

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

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。在声明一个切入点(切入点约定切片在哪些方法上起作用,在什么时候上起作用)
切入点常用的注解(约定在什么时候起作用)

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

    //此注解声明类为切面
@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处理。它可以在两个不同的线程之间来传递。其大致处理流程如下:

    @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;
    }
}

最需要理解的是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 {

}

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

上一篇下一篇

猜你喜欢

热点阅读