java restful api开发_SpringBoot快速开发Restful Api

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测试用例的方法。首先导入依赖

org.springframework.boot

spring-boot-starter-test

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{

//在这个类中可以使用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 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> error(HttpServletRequest request) {

Map 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 HandlerServiceException(ServiceException e){

Map 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 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 Order() throws InterruptedException {

logger.info("主线程开始");

//使用Callable单开一个线程处理

Callable result=new Callable() {

@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> map=new HashMap<>();

public Map> getMap() {

return map;

}

public void setMap(Map> 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 {

}

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

你可能感兴趣的:(java,restful,api开发)