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常用校验注解
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会返回一个默认的错误页面,如下所示:
但使用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机制就不会有明显的效果。如下
此时需要使用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 {
}
需要特别注意的是配置类中的属性名必须和配置项的名称完全相同,否则将无法正常读取