This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

ESA Restlight

ESA Restlight is a lightweight and rest-oriented web framework, which supports annotations of SpringMVC and JAX-RS.

Why Restlight?

In microservices, it is generally expected that service is a lightweight application, and the Spring-Web framework we often use is really a good framework that allows us to develop a variety of applications easily, but it is too bloated for applications in microservices, which just need a rest supports and do not need the session, ModelAndview, JSP and so on… Restlight aims to serve as a web framework that helps users to build a high performance and lightweight microservice.

What kind of web application is Restlight suitable for?

  • Rest application
  • High-performance requirements
  • Middleware
  • HTTP proxy
  • Any application that needs HTTP services

What kind of web application is Restlight not suitable for?

  • All in one application
  • Servlet requirements: Restlight does not support servlet standards

Env Requirements

Name Version
Java JDK8+
Spring Boot 2.1.0.RELEASE+
Netty 4.1.52.Final+
Tcnative 2.0.34.Final+ and matches with Netty

Features

  • Annotations of SpringMVC and JAX-RS supports
  • High performance
  • Reactive:CompletableFutureListenableFuture(Guava).
  • Threading-model: Flexible scheduling between IO EventLoopGroup and Biz Schedulers.
  • Http1/Http2/H2c/Https
  • HAProxy
  • Filter
  • Interceptor
  • JSR-303: hibernate-validator
  • Self-protection: Connection creation limit, Cpu Load protection
  • more …

Release Notes

Releases

1 - Getting Started

It’s very easy to get started with Restlight!

Create a Spring Boot application and add dependency

Note:netty 4.1.56.Final and tcnative 2.0.35.Final are directly dependent on.

Note: Please make sure the version of tcnative matches the version of netty.

<dependency>
    <groupId>io.esastack</groupId>
    <artifactId>restlight-starter</artifactId>
    <version>${mvn.version}</version>
</dependency>

Write your Controller

@RestController
@SpringBootApplication
public class RestlightDemoApplication {

    @GetMapping("/hello")
    public String hello() {
        return "Hello Restlight!";
    }

    public static void main(String[] args) {
        SpringApplication.run(RestlightDemoApplication.class, args);
    }
}

Run your application and then you would see something like

Started Restlight server in 1265 millis on 0.0.0.0:8080

curl http://localhost:8080/hello

2 - Architecture

设计原则

  • 高兼容性
  • 极致性能
  • 全链路异步
  • 易用
  • 可扩展

功能架构

分层架构设计

Architecture

架构图中ESA HttpServer, Restlight Server, Restlight Core, Restlight for Spring, Restlight Starter几个模块均可作为一个独立的模块使用, 满足不同场景下的需求。

ESA HttpServer

基于Netty 实现的一个简易的HttpServer, 支持Http1.1/Http2以及Https等

Restlight Server

ESA HttpServer基础之上封装了

  • 引入业务线程池
  • Filter
  • 请求路由(根据url, method, header等条件将请求路由到对应的Handler)
  • 基于CompletableFuture的响应式编程支持
  • 线程调度

对应启动入口类esa.restlight.server.Restlite

Restlight Core

Restlight Server之上, 扩展支持了Controller方式(在Controller类中通过诸如@RequestMappng等注解的方式构造请求处理逻辑)完成业务逻辑以及诸多常用功能

  • HandlerInterceptor: 拦截器
  • ExceptionHandler: 全局异常处理器
  • BeanValidation: 参数校验
  • ArgumentResolver: 参数解析扩展
  • ReturnValueResolver: 返回值解析扩展
  • RequestSerializer: 请求序列化器(通常负责反序列化Body内容)
  • ResposneSerializer: 响应序列化器(通常负责序列化响应对象到Body)
  • 内置Jackson, Fastjson, Gson, ProtoBuf序列化支持

对应启动入口类esa.restlight.core.Restlight

Restlight SpringMVC & Restlight JAX-RS

  • Restlight SpringMVCSpringMVC中的注解使用习惯的Restlight Core的扩展实现(@RequestMapping, @RequestParam等)。

  • Restlight JAX-RSJAX-RS中的注解使用习惯的Restlight Core的扩展实现(@Path, @GET, @QueryParam等)。

Restlight for Spring

Restlight Core基础上支持在Spring场景下通过ApplicationContext容器自动配置各种内容(RestlightOptions, 从容器中自动配置Filter, Controller, ControllerAdvice等)

Restlight Starter

Restlight for Spring基础上支持在Spring Boot场景的自动配置

Restlight Actuator

Restlight Starter基础上支持在Spring Boot Actuator原生各种Endpoints支持以及Restlight独有的Endpoints。

3 - 线程模型

ThreadingModel

Restlight由于是使用Netty作为底层HttpServer的实现,因此图中沿用了部分EventLoop的概念,线程模型由了AcceptorIO EventLoopGroup(IO线程池)以及Biz ThreadPool(业务线程池)组成。

  • Acceptor: 由1个线程组成的线程池, 负责监听本地端口并分发IO 事件。
  • IO EventLoopGroup: 由多个线程组成,负责读写IO数据(对应图中的read()write())以及HTTP协议的编解码和分发到业务线程池的工作。
  • Biz Scheduler:负责执行真正的业务逻辑(大多为Controller中的业务处理,拦截器等)。
  • Custom Scheduler: 自定义线程池

4 - Restlight Starter

Restlight StarterRestlight Spring 基础上封装的Spring Boot Starter提供基于Spring Boot的自动配置

4.1 - Thread-Scheduling

ThreadingModel

线程调度允许用户根据需要随意制定ControllerIO线程上执行还是在Biz线程上执行还是在自定义线程上运行。

使用@Scheduled注解进行线程调度

eg.

IO线程上执行

@Scheduled(Schedulers.IO)
@GetMapping("/foo")
public String io() {
    // ....
    return "";
}

BIz线程上执行

// 在业务线程池中执行
@Scheduled(Schedulers.BIZ)
@GetMapping("/bar")
public String biz() {
    // ....
    return "";
}

不加注解默认Scheduler上执行

@GetMapping("/baz")
public String bizBatching() {
    // ....
    return "";
}

自定义Scheduler

Scheduler实现注入Spring

@Bean
public Scheduler scheduler() {
    return Schedulers.fromExecutor("foo", Executors.newCachedThreadPool());
}

在自定义Scheduler上执行

// 在业务线程池中执行
@Scheduled("foo")
@GetMapping("/foo")
public String foo() {
    // ....
    return "";
}

配置

所有配置均以restlight.server.scheduling开头

配置项 默认 说明
default-scheduler BIZ 在不加@Scheduled注解时采用的Scheduler

4.2 - Filter

基本使用

@Bean
public Filter addHeaderFilter() {
    return new Filter() {
        @Override
        public CompletableFuture<Void> doFilter(AsyncRequest request, AsyncResponse response, FilterChain chain) {
            request.setHeader("foo", "bar");
            return chain.doFilter(request, response);
        }
    };
}

上面的例子将会给所有到来的请求都加上一个固定的Header。

异步Filter

@Bean
public Filter filter() {
    return new Filter() {
        @Override
        public CompletableFuture<Void> doFilter(AsyncRequest request, AsyncResponse response, FilterChain chain) {
            return CompletableFuture.runAsync(() -> {
                // do something...
            }).thenCompose(r -> {
                // invoke next filter
                return chain.doFilter(request, response);
            });
        }
    };
}

上面的例子演示了在doFilter(xxx)中进行异步操作,并在该操作完成后回调FilterChain继续执行后续操作。

终止Filter的执行

当不期望执行后续的Filter时可返回一个CompletableFuture.completedFuture(null)实例。

@Override
public CompletableFuture<Void> doFilter(AsyncRequest request, AsyncResponse response, FilterChain chain) {
    return CompletableFuture.completedFuture(null);
}

4.3 - 拦截器

Restlight支持多种拦截器,适用于不同性能/功能场景

  • RouteInterceptor
  • MappingInterceptor
  • HandlerInterceptor
  • InterceptorFactory

拦截器定位

面向Controller/Route, 同时支持按需匹配的拦截器

  1. 面向Controller/Route: 拦截器一定能让用户根据需求选择匹配到哪个Controller接口
  2. 按需匹配: 支持按照AsyncRequest条件让用户灵活决定拦截器的匹配规则

InternalInterceptor

核心拦截器实现, 封装了拦截器核心行为

  • CompletableFuture<Boolean> preHandle0(AsyncRequest, AsyncResponse, Object)

    Controller执行前执行。返回布尔值表示是否允许当前请求继续往下执行。

  • CompletableFuture<Void> postHandle0(AsyncRequest, AsyncResponse, Object)

    Controller刚执行完之后执行

  • CompletableFuture<Void> afterCompletion0(AsyncRequest, AsyncResponse, Exception)

    请求行完之后执行

  • int getOrder()

    返回当前拦截器的优先级(默认为最低优先级),我们保证getOrder返回值决定拦截器的执行顺序,但是不支持使用@Order(int)注解情况下的顺序(虽然有时候看似顺序和@Order一致,但那只是巧合)。

拦截器匹配

初始化阶段: 初始化阶段为每一个Controller确定所有可能匹配到当前Controller的拦截器列表(包含可能匹配以及一定会匹配到当前Controller的拦截器)。

运行时阶段: 一个请求AsyncRequest到来时将通过将AsyncRequest作为参数传递到拦截器做路由判定, 决定是否匹配。

Affinity亲和性

public interface Affinity {

    /**
     * Current component is always attaching with the target subject.
     */
    int ATTACHED = 0;

    /**
     * Current component has a high affinity with the target subject this is the highest value.
     */
    int HIGHEST = 1;
    /**
     * Current component has no affinity with the target subject.
     */
    int DETACHED = -1;

    /**
     * Gets the affinity value.
     *
     * @return affinity.
     */
    int affinity();
}

用于表达拦截器与Controller/Route之间的亲和性

  • affinity()小于0表示拦截器不可能Controller匹配
  • affinity()等于0表示拦截器一定会与Controller匹配
  • affinity()大于0表示拦截器可能会与Controller匹配, 并且值越小匹配可能性越高(因此1为最高可能性), 相反值越大匹配可能性越小, 同时匹配的开销越大(用于拦截器匹配性能优化)。

InterceptorPredicate

public interface InterceptorPredicate extends RequestPredicate {
    InterceptorPredicate ALWAYS = request -> Boolean.TRUE;
}

public interface RequestPredicate extends Predicate<AsyncRequest> {
    // ignore this
    default boolean mayAmbiguousWith(RequestPredicate another) {
        return false;
    }
}

test(AsyncRequest)方法用于对每个请求的匹配。可以满足根据AsyncRequest运行时的任意条件的匹配(而不仅仅是局限于URL匹配)

Interceptor

Interceptor接口为同时拥有AffinityController/Route匹配)以及InterceptorPredicate(请求AsyncRequest匹配)的接口

public interface Interceptor extends InternalInterceptor, Affinity {

    /**
     * Gets the predicate of current interceptor. determines whether current interceptor should be matched to a {@link
     * esa.httpserver.core.AsyncRequest}.
     *
     * @return predicate, or {@code null} if {@link #affinity()} return's a negative value.
     */
    InterceptorPredicate predicate();

    /**
     * Default to highest affinity.
     * <p>
     * Whether a {@link Interceptor} should be matched to a {@link esa.restlight.server.route.Route} is depends on it.
     *
     * @return affinity
     */
    @Override
    default int affinity() {
        return HIGHEST;
    }
}

由于int affinity()根据不同的Controller/Route可能得出不同的结果, 因此需要使用InterceptorFactory进行创建

eg.

实现一个拦截器, 拦截所有GET接口(仅包含GET)且Header中包含X-Foo请求头的请求

@Bean
public InterceptorFactory interceptor() {
    return (ctx, route) -> new Interceptor() {
        @Override
        public CompletableFuture<Boolean> preHandle0(AsyncRequest request,
                                                     AsyncResponse response,
                                                     Object handler) {
            // biz logic
            return CompletableFuture.completedFuture(true);
        }


        @Override
        public InterceptorPredicate predicate() {
            return request -> request.containsHeader("X-Foo");
        }

        @Override
        public int affinity() {
            HttpMethod[] method = route.mapping().method();
            if (method.length == 1 && method[0] == HttpMethod.GET) {
                return ATTACHED;
            }
            return DETACHED;
        }
    };
}

RouteInterceptor

只绑定到固定的Controller/Route的拦截器

public interface RouteInterceptor extends InternalInterceptor {

    /**
     * Gets the affinity value between current interceptor and the given {@link Route}.
     *
     * @param ctx   context
     * @param route route to match
     *
     * @return affinity value.
     */
    boolean match(DeployContext<? extends RestlightOptions> ctx, Route route);
}

eg. 实现一个拦截器, 拦截所有GET请求(仅包含GET)

@Bean
public RouteInterceptor interceptor() {
    return new RouteInterceptor() {

        @Override
        public CompletableFuture<Boolean> preHandle0(AsyncRequest request,
                                                     AsyncResponse response,
                                                     Object handler) {
            // biz logic
            return CompletableFuture.completedFuture(true);
        }

        @Override
        public boolean match(DeployContext<? extends RestlightOptions> ctx, Route route) {
            HttpMethod[] method = route.mapping().method();
            return method.length == 1 && method[0] == HttpMethod.GET;
        }
    };
}

MappingInterceptor

绑定到所有Controller/Route, 并匹配请求的拦截器

public interface MappingInterceptor extends InternalInterceptor, InterceptorPredicate {
}

相当于affinity()固定返回Affinity.ATTACHED

eg.

实现一个拦截器, 拦截所有Header中包含X-Foo请求头的请求

@Bean
public MappingInterceptor interceptor() {
    return new MappingInterceptor() {

        @Override
        public CompletableFuture<Boolean> preHandle0(AsyncRequest request,
                                                     AsyncResponse response,
                                                     Object handler) {
            // biz logic
            return CompletableFuture.completedFuture(true);
        }
        
        @Override
        public boolean test(AsyncRequest request) {
            return request.containsHeader("X-Foo");
        }
    };
}

HandlerInterceptor

支持基于URI匹配的拦截器接口

  • includes(): 指定拦截器作用范围的Path, 默认作用于所有请求。
  • excludes(): 指定拦截器排除的Path(优先级高于includes)默认为空。
public interface HandlerInterceptor extends InternalInterceptor {

    String PATTERN_FOR_ALL = "/**";

    default String[] includes() {
        return null;
    }

    default String[] excludes() {
        return null;
    }

}

eg.

实现一个拦截器, 拦截除/foo/bar意外所有/foo/开头的请求

@Bean
public HandlerInterceptor interceptor() {
    return new HandlerInterceptor() {

        @Override
        public CompletableFuture<Boolean> preHandle0(AsyncRequest request,
                                                     AsyncResponse response,
                                                     Object handler) {
            // biz logic
            return CompletableFuture.completedFuture(true);
        }

        @Override
        public String[] includes() {
            return new String[] {"/foo/**"};
        }

        @Override
        public String[] excludes() {
            return new String[] {"/foo/bar"};
        }
    };
}

InterceptorFactory

拦截器工厂类, 用于为每一个Controller/Route生成一个Interceptor

public interface InterceptorFactory {

    /**
     * Create an instance of {@link Interceptor} for given target handler before starting Restlight server..
     *
     * @param ctx deploy context
     *
     * @param route target route.
     * @return interceptor
     */
    Optional<Interceptor> create(DeployContext<? extends RestlightOptions> ctx, Route route);
}

4.4 - 异常处理

Spring MVC异常处理

Restlight for Spring MVC支持使用Spring MVC中的@ExceptionHandler, @ControllerAdvice等注解,并且对此能力进行了增强 参考ExceptionHandler支持

ExceptionResolver

eg.

处理RuntimeException

@Component
public class GlobalExceptionResolver implements ExceptionResolver<RuntimeException> {
    
    @Override
    public CompletableFuture<Void> handleException(AsyncRequest request,
                                                   AsyncResponse response,
                                                   RuntimeException e) {
        // handle exception here
        return CompletableFuture.completedFuture(null);
    }
}

4.5 - 参数解析

参数解析指将从请求中解析出Controller参数值的过程(包含反序列化)

典型的有

  • @RequestParam
  • @RequestHeader
  • @RequestBody
  • @PathVariable
  • @CookieValue
  • @MatrixVariable
  • @QueryBean
  • AsyncRequest
  • AsyncResponse

接口定义

public interface ArgumentResolver {

    /**
     * 从AsyncRequest中解析出对应参数的值
     * 解析后的Object必须能和Param类型匹配
     */
    Object resolve(AsyncRequest request, AsyncResponse response) throws Exception;
    
}

框架会在启动的初始化阶段试图为每一个Controller中的每一个参数都找到一个与之匹配的ArgumentResolver用于请求的参数解析。

框架如何确定ArgumentResolver

ArgumentResolverAdapter


public interface ArgumentResolverPredicate {

    /**
     * 判断当前ArgumentResolver是否支持给定Param解析
     * Controller中的每个参数都对应一个Param实例, 
     * 可以通过Param获取注解等各类反射相关的元数据信息
     */
    boolean supports(Param param);

}

public interface ArgumentResolverAdapter extends ArgumentResolverPredicate, ArgumentResolver, Ordered {
    @Override
    default int getOrder() {
        return HIGHEST_PRECEDENCE;
    }

}

初始化逻辑:

  1. 按照getOrder()方法的返回将Spring容器中所有的ArgumentResolver进行排序
  2. 按照排序后的顺序依次调用supports(Param param)方法, 返回true则将其作为该参数的ArgumentResolver, 运行时每次请求都将调用resolve(AsyncRequest request, AsyncResponse response)方法进行参数解析, 并且运行时不会改变。
  3. 未找到则启动报错。

细心的人可能会发现该设计可能并不能覆盖到以下场景

  • 因为resolve(AsyncRequest request, AsyncResponse response)方法参数中并没有传递Param参数, 虽然初始化阶段能根据supports(Param param)方法获取参数元数据信息(获取某个注解, 获取参数类型等等)判断是否支持, 但是如果运行时也需要获取参数的元数据信息(某个注解的值等)的话,此接口则无法满足需求。
  • 假如ArgumentResolver实现中需要做序列化操作, 因此期望获取到Spring容器中的序列化器时,则该接口无法支持(例如@RequestBody的场景)。

针对以上问题, 答案是确实无法支持。因为Restlight的设计理念是

  • 能在初始化阶段解决的问题就在初始化阶段解决

因此不期望用户以及Restlight的开发人员大量的在运行时去频繁获取一些JVM启动后就不会变动的内容(如: 注解的值), 甚至针对某些元数据信息使用ConcurrentHashMap进行缓存(看似是为了提高性能的缓存, 实际上初始化就固定了的内容反而增加了并发性能的损耗)。

基于以上原因我们提供了另一个ArgumentResolver的实现方式

ArgumentResolverFactory

public interface ArgumentResolverFactory extends ArgumentResolverPredicate, Ordered {

    ArgumentResolver createResolver(Param param,
                                    List<? extends HttpRequestSerializer> serializers);

    @Override
    default int getOrder() {
        return HIGHEST_PRECEDENCE;
    }
}

与上面的ArgumentResolver类似

初始化逻辑:

  1. 按照getOrder()方法的返回将所有的ArgumentResolverFactory进行排序
  2. 按照排序后的顺序依次调用supports(Param param)方法, 返回true则将其作为该参数的ArgumentResolverFactory, 同时调用createResolver(Param param, List<? extends HttpRequestSerializer> serializers)方法创建出对应的ArgumentResolver
  3. 未找到则启动报错。

由于初始化时通过createResolver(Param param, List<? extends HttpRequestSerializer> serializers)方法传入了Param以及序列化器, 因此能满足上面的要求。

两种模式的定位

  • ArgumentResolver: 适用于参数解析器不依赖方法元数据信息以及序列化的场景。例如: 如果参数上使用了@XXX注解则返回某个固定的值。
  • ArgumentResolverFactory: 适用于参数解析器依赖方法元数据信息以及序列化的场景。例如: @RequestBody@RequestParameter(name = "foo")

自定义参数解析器

将自定义实现的ArgumentResolverAdapter或者ArgumentResolverFactory注入到Spring容器即可。

  • ArgumentResolverAdapter案例

场景: 当参数上有@AppId注解时, 使用固定的AppId

// 自定义注解
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AppId {
}
@Bean
public ArgumentResolverAdapter resolver() {
    return new ArgumentResolverAdapter() {
        @Override
        public boolean supports(Param param) {
            // 当方法上有此注解时生效
            return param.hasParameterAnnotation(AppId.class);
        }
 
        @Override
        public Object resolve(AsyncRequest request, AsyncResponse response) {
            return "your appid";
        }
    };
}

controller使用


@GetMapping("/foo")
public String foo(@AppId String appId) {
    return appId;
}

上面的代码自定义实现了依据自定义注解获取固定appId的功能

  • ArgumentResolverFactory

场景: 通过自定义注解获取固定前缀x-custom的Header

// 自定义注解
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomHeader {
    String value();
}
@Bean
public ArgumentResolverFactory resolver() {
    return new ArgumentResolverFactory() {
        @Override
        public boolean supports(Param param) {
            // 当方法上有此注解时生效
            return param.hasParameterAnnotation(CustomHeader.class);
        }

        @Override
        ArgumentResolver createResolver(Param param,
                                        List<? extends HttpRequestSerializer> serializers) {
            return new Resolver(param);
        }
    };
}

/**
 * 实际ArgumentResolver实现
 */
private static class Resolver implements ArgumentResolver {

    private final String headerName;

    private Resolver(Param param) {
        CustomHeader anno = param.getParameterAnnotation(CustomHeader.class);
        if (anno.value().length() == 0) {
            throw new IllegalArgumentException("Name of header must not be empty.");
        }
        // 初始化时组装好需要的参数
        this.headerName = "x-custom" + anno.value();
    }

    @Override
    public Object resolve(AsyncRequest request, AsyncResponse response) {
        // 运行时直接获取Header
        return request.getHeader(headerName);
    }
}

controller使用


@GetMapping("/foo")
public String foo(@CustomHeader("foo") String foo) {
    return foo;
}

4.6 - ArgumentResolverAdvice

ArgumentResolverAdvice允许用户在ArgumentResolver参数解析器解析参数的前后添加业务逻辑以及修改解析后的参数。

接口定义

public interface ArgumentResolverAdvice {

    /**
     * 在ArgumentResolver.resolve()之前被调用
     */
    void beforeResolve(AsyncRequest request, AsyncResponse response);

    /**
     * 在ArgumentResolver.resolve()之后被调用, 并使用此方法的返回值作为参数绑定到对应的Controller参数上
     */
    Object afterResolved(Object arg, AsyncRequest request, AsyncResponse response);
}

自定义ArgumentResolverAdvice

ArgumentResovler相同, ArgumentResolverAdvice自定应时同样需要实现ArgumentResolverAdviceAdapterArgumentResolverAdviceFactory接口

方式1:实现 ArgumentResolverAdviceAdapter

接口定义

public interface ArgumentResolverAdviceAdapter
        extends ArgumentResolverPredicate, ArgumentResolverAdvice, Ordered {

    @Override
    default void beforeResolve(AsyncRequest request, AsyncResponse response) {
    }

    @Override
    default Object afterResolved(Object arg, AsyncRequest request, AsyncResponse response) {
        return arg;
    }

    @Override
    default int getOrder() {
        return HIGHEST_PRECEDENCE;
    }
}

方式2:实现ArgumentResolverAdviceFactory

接口定义

public interface ArgumentResolverAdviceFactory extends ArgumentResolverPredicate, Ordered {
    /**
     * 生成ArgumentResolverAdvice
     */
    ArgumentResolverAdvice createResolverAdvice(Param param, ArgumentResolver resolver);

    @Override
    default int getOrder() {
        return HIGHEST_PRECEDENCE;
    }
}

ArgumentResolverAdviceAdapter接口以及ArgumentResolverAdviceFactory接口与ArgumentResolver中的ArgumentResolverAdapter接口以及ArgumentResolverFactory接口的使用方式相同, 这里不过多赘述。

4.7 - 返回值解析

返回值解析指将Controller返回值写入到AsyncResponse中的过程(包含序列化)

返回值解析逻辑

Restlight默认支持的返回值解析方式包括

  • @ResponseBody
  • @ResponseStatus
  • 普通类型(String, byte[], int 等)

与参数解析类似, 每个功能都对应了一个返回值解析器的实现。

接口定义

public interface ReturnValueResolver {

    /**
     * 解析出对应返回值为byte[]
     */
    byte[] resolve(Object returnValue,
                   AsyncRequest request,
                   AsyncReponse response) throws Exception;

}

框架会在启动的初始化阶段试图为每一个Controller中的每一个参数都找到一个与之匹配的ReturnValueResolver用于响应的返回值解析。

框架如何确定ReturnValueResolver

ReturnValueResolverAdapter


public interface ReturnValueResolverPredicate {

    /**
     * 判断当前ReturnValueResolver是否支持给定InvocableMethod解析
     * 每一个Controller都对应一个InvocableMethod实例, 
     * 可以通过InvocableMethod获取注解, 返回值类型等各类反射相关的元数据信息
     */
    boolean supports(InvocableMethod invocableMethod);

}

public interface ReturnValueResolverAdapter extends ReturnValueResolverPredicate, ReturnValueResolver, Ordered {

    @Override
    default int getOrder() {
        return HIGHEST_PRECEDENCE;
    }

}

初始化逻辑:

  1. 按照getOrder()方法的返回将Srping容器中所有的ReturnValueResolverAdapter进行排序
  2. 按照排序后的顺序依次调用supports(InvocableMethod invocableMethod)方法, 返回true则将其作为该参数的ReturnValueResolver, 运行时每次请求都将调用resolve(Object returnValue, AsyncRequest request, AsyncReponse response)方法进行参数解析, 并且运行时不会改变。
  3. 未找到则启动报错。

细心的人可能会发现该设计可能并不能覆盖到以下场景

  • 因为resolve(Object returnValue, AsyncRequest request, AsyncReponse response)方法参数中并没有传递InvocableMethod参数, 虽然初始化阶段能根据supports(InvocableMethod invocableMethod)方法获取Controller方法元数据信息(获取某个注解, 获取参数类型等等)判断是否支持, 但是如果运行时也需要获取Controller方法的元数据信息(某个注解的值等)的话,此接口则无法满足需求。
  • 假如ReturnValueResolverAdapter实现中需要做序列化操作, 因此期望获取到Spring容器中的序列化器时,则该接口无法支持(例如@ResponseBody的场景)。

针对以上问题, 答案是确实无法支持。因为Restlight的设计理念是

  • 能在初始化阶段解决的问题就在初始化阶段解决

因此不期望用户以及Restlight的开发人员大量的在运行时去频繁获取一些JVM启动后就不会变动的内容(如: 注解的值), 甚至针对某些元数据信息使用ConcurrentHashMap进行缓存(看似是为了提高性能的缓存, 实际上初始化就固定了的内容反而增加了并发性能的损耗)。

基于以上原因我们提供了另一个ReturnValueResolver的实现方式

ReturnValueResolverFactory

public interface ReturnValueResolverFactory extends ReturnValueResolverPredicate, Ordered {

    ReturnValueResolver createResolver(InvocableMethod invocableMethod,
                                    List<? extends HttpResponseSerializer> serializers);

    @Override
    default int getOrder() {
        return HIGHEST_PRECEDENCE;
    }
}

与上面的ReturnValueResolver类似

初始化逻辑:

  1. 按照getOrder()方法的返回将所有的ReturnValueResolverFactory进行排序
  2. 按照排序后的顺序依次调用supports(InvocableMethod invocableMethod)方法, 返回true则将其作为该参数的ReturnValueResolverFactory, 同时调用createResolver(InvocableMethod invocableMethod, List<? extends HttpResponseSerializer> serializers)方法创建出对应的ReturnValueResolver
  3. 未找到则启动报错。

由于初始化时通过createResolver(InvocableMethod invocableMethod, List<? extends HttpResponseSerializer> serializers)方法传入了InvocableMethod以及序列化器, 因此能满足上面的要求。

两种模式的定位

  • ReturnValueResolver: 适用于解析器不依赖方法元数据信息以及序列化的场景。例如: 如果Controller方法上上使用了@XXX注解则返回某个固定的值。
  • ReturnValueResolverFactory: 适用于解析器依赖方法元数据信息以及序列化的场景。例如: @ResponseBody, @ResponseStatus(reason = “error”)。

自定义返回值解析器

将自定义实现的ReturnValueResolverAdapter或者ReturnValueResolverFactory注入到Spring容器即可。

  • ReturnValueResolverAdapter案例

场景: 当Controller方法上有@AppId注解时, 返回固定的AppId

// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AppId {
}
@Bean
public ReturnValueResolverAdapter resolver() {

    private static byte[] APP_ID = "your appid".getBytes(StandardCharsets.UTF_8);

    return new ReturnValueResolverAdapter() {
        @Override
        public boolean supports(InvocableMethod invoicableMethod) {
            // 当方法上有此注解时生效
            return invoicableMethod.hasMethodAnnotation(AppId.class);
        }
 
        @Override
        public byte[] resolve(Object returnValue,
                   AsyncRequest request,
                   AsyncReponse response) {
            return APP_ID;
        }
    };
}

controller使用


@GetMapping("/foo")
@AppId
public String foo() {
    return "";
}

上面的代码自定义实现了依据自定义注解获取固定appId的功能

  • ReturnValueResolverFactory

场景: 通过自定义注解对所有String类型的返回值加上一个指定前缀。

// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Suffix {
    String value();
}
@Bean
    public ReturnValueResolverFactory resolver() {
        return new ReturnValueResolverFactory() {
            @Override
            public boolean supports(InvocableMethod invocableMethod) {
                // 当方法上有此注解时生效
                return String.class.equals(invocableMethod.getMethod().getReturnType()) && parameter.hasMethodAnnotation(CustomHeader.class);
            }
            
            public ReturnValueResolver createResolver(InvocableMethod invocableMethod,
                                                      List<? extends HttpResponseSerializer> serializers) {
                return new Resovler(invocableMethod);
            }
            
        };
    }

    /**
     * 实际ArgumentResolver实现
     */
    private static class Resolver implements ReturnValueResolver {

        private final String suffix;

        private Resolver(InvocableMethod invocableMethod) {
            // 获取前缀
            Suffix anno = invocableMethod.getMethodAnnotation(Suffix.class);
            this.suffix = anno.value();
        }

        @Override
        public byte[] resolve(Object returnValue,
                   AsyncRequest request,
                   AsyncReponse response) {
            // 拼接
            return suffix + String.valueOf(returnValue);
        }
    }

controller使用


@GetMapping("/foo")
@Suffix
public String foo() {
    return "foo";
}

4.8 - ReturnValueResolverAdvice

ReturnValueResolverAdvice允许用户在ReturnValueResolver参数解析器解析参数的前后添加业务逻辑以及修改解析后的参数。

接口定义

public interface ReturnValueResolverAdvice {
    /**
     * 使用此方法的返回值作为ReturnValueResolver.resolve()的参数调用
     */
    Object beforeResolve(Object returnValue, AsyncRequest request, AsyncResponse response);
}

自定义ReturnValueResolverAdvice

ArgumentResovler相同, ReturnValueResolverAdvice自定应时同样需要实现ReturnValueResolverAdviceAdapterReturnValueResolverAdviceFactory接口

方式1 实现ReturnValueResolverAdviceAdapter

接口定义

public interface ReturnValueResolverAdviceAdapter
        extends ReturnValueResolverPredicate, ReturnValueResolverAdvice, Ordered {

    @Override
    default Object beforeResolve(Object returnValue, AsyncRequest request, AsyncResponse response) {
        return returnValue;
    }

    @Override
    default int getOrder() {
        return HIGHEST_PRECEDENCE;
    }
}

方式2:实现ReturnValueResolverAdviceFactory

接口定义

public interface ReturnValueResolverAdviceFactory extends ReturnValueResolverPredicate, Ordered {
    /**
     * 生成ReturnValueResolverAdvice
     */
    ReturnValueResolverAdvice createResolverAdvice(InvocableMethod method, ReturnValueResolver resolver);

    @Override
    default int getOrder() {
        return HIGHEST_PRECEDENCE;
    }
}

ReturnValueResolverAdviceAdapter接口以及ReturnValueResolverAdviceFactory接口与ReturnValueResolver中的ReturnValueResolverAdapter接口以及ReturnValueResolverFactory接口的使用方式相同, 这里不过多赘述。

4.9 - 序列化

序列化支持

  • jackson(默认)
  • fastjson
  • Gson
  • ProtoBuf
  • 自定义支持

序列化切换

默认使用Jackson序列化,因此Restlight也默认引入了Jackson依赖,如果想要切换成别的序列化方式,则需要引入对应的Maven依赖并进行简单的配置。

Example:以切换Gson为例

  1. 引入Gson依赖:
<dependency>
     <groupId>com.google.code.gson</groupId>
     <artifactId>gson</artifactId>
     <version>2.8.5</version>
</dependency>
  1. 注入Gson序列化器
@Bean
public HttpBodySerializer bodySerializer() {
    return new GsonHttpBodySerializer();
}

完成切换。

序列化器

接口标准定义

  • HttpRequestSerializer: 用于请求的反序列化。
  • HttpResponseSerializer: 用于响应的序列化。
  • HttpBodySerializer: 继承自HttpRequestSerializer以及HttpResponseSerializer, 可同时提供请求与响应的序列化功能。

内置的序列化器

  • FastJsonHttpBodySerializer
  • JacksonHttpBodySerializer
  • GsonHttpBodySerializer
  • ProtoBufHttpBodySerializer

定制序列化器

可以选择通过继承或者自定义实现HttpRequestSerializer以及HttpResponseSerializer接口(或者HttpResponseSerializer同理)的方式实现定制序列化。

如,在上面的例子中我们切换序列化方式为jackson,此时我们想定制序列化的日期格式为yyyy-MM-dd

@Bean
public HttpBodySerializer bodySerializer() {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd"));
    return new HttpJsonBodySerializerAdapter(new JacksonSerializer(objectMapper)) {};
}

同样也可以定制更多的序列化选项。

多个序列化器并存

Restlight并不像Spring MVC一样要求一个应用中只能使用一种序列化方式,Restlight允许多种序列化方式并存。

实际的服务中我们可能有这样的需求:我们对接服务A时使用的是ProtoBuf序列化,而其他情况都是使用的fastjson序列化。

配置如下:

@Bean
public HttpBodySerializer fastjsonBodySerializer() {
    return new FastJsonHttpBodySerializer();
 
@Bean
public HttpBodySerializer protoBufBodySerializer() {
    return new ProtoBufHttpBodySerializer();
}

直接注入两个序列化器即可。

上面的配置当MediaType(请求对应ContentType, 响应对应Accept)为application/json时使用JSON序列化, 为application/x-protobuf时使用ProtoBuf序列化.

兼容Spring Boot标准

使用Spring Boot时,经常在配置文件里面添加Jackson和Gson的配置,如:

spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.gson.date-format=yyyy-MM-dd HH:mm:ss

Restlight同样支持该标准,其作用逻辑如下图所示: 序列化器兼容springboot逻辑图.png 如图所示,配置文件中Jackson和Gson相关内容要想生效,需要满足如下条件:

  1. 用户没有手动定制HttpJsonBodySerializerAdapter
  2. 用户没有手动注入Jackson或者Gson序列化器(FastJsonHttpBodySerializer和GsonJsonHttpBodySerializer)
  3. 用户没有手动注入ObjectMapper或者Gson对象
  4. 配置文件有对Jackson和Gson进行配置

满足以上四个条件,配置文件中关于Jackson和Gson的配置,才会在对应的Jackson和Gson序列化器中生效。

响应序列化内容协商(Json与ProtoBuf序列化)

当 json和ProtoBuf的序列化器同时存在时, 响应序列化方式可以通过url中的参数来指定。

  • 首先开启序列化协商功能:
# 开启请求序列化协商
restlight.server.serialize.request.negotiation=true
# 开启响应序列化协商
restlight.server.serialize.response.negotiation=true

指定使用json序列化

eg: /foo/{bar}?format=json

指定使用protobuf序列化

eg: /foo/{bar}?format=pb

  • 当参数format与应用自身的某些接口中的参数冲突时可以通过配置修改参数名称
restlight.server.serialize.request.negotiation-param=your-param-name
restlight.server.serialize.response.negotiation-param=your-param-name

使用@RequestSerializer指定请求序列化器

当配置了多个序列化器时, 可以使用@RequestSerializer中指定使用某个固定的序列化方式

下面的Controller指定使用Jackson反序列化请求数据

@PostMapping("/foo")
public void bar(@RequestBody @RequestSerializer(JacksonHttpBodySerializer.class) User user) {
    // ...
}

使用@ResponseSerializer指定响应序列化器

当配置了多个序列化器时, 可以使用@ResponseSerializer中指定使用某个固定的序列化方式

下面的Controller指定使用Jackson序列化响应结果

@PostMapping("/foo")
@ResponseSerializer(JacksonHttpBodySerializer.class)
public Foo bar() {
    return new Foo();
}

使用@Serializer指定请求 & 响应序列化器

当配置了多个序列化器时, 可以使用@Serializer中指定使用某个固定的序列化方式, 相当于同时指定RequestSerializer以及@ResponseSerializer

下面的Controller指定使用Jackson序列化

@PostMapping("/foo")
@Serializer(JacksonHttpBodySerializer.class)
public Foo bar(@RequestBody User user) {
    return new Foo();
}

多个序列化器框架如何选择使用哪个序列化器实现?

序列化器接口HttpRequestSerializerHttpResponseSerializer中均定义了suppot(xxx)方法, 用于在多个序列化器并存的情况下判断使用哪一个序列化器。以HttpRequestSerializer为例

public interface HttpRequestSerializer extends BaseHttpSerializer, RxSerializer {
    
    boolean supportsRead(MediaType mediaType, Type type);
    
}

HttpRequestSerializer中定义了supportsRead(MediaType mediaType, Type type)方法用于判断当前序列化器是否支持此次请求的序列化, 其中参数MediaType为请求的ContentTypeType为当前反序列化的目标类型。

同样HttpResponseSerializer

public interface HttpResponseSerializer extends BaseHttpSerializer, TxSerializer {

    boolean supportsWrite(MediaType mediaType, Type type);

    Object customResponse(AsyncRequest request, AsyncResponse response, Object returnValue);

}
  • supportsWrite(MediaType mediaType, Type type)方法用于判断当前序列化器是否支持此次请求的序列化, 其中参数MediaType为请求的Accept字段中的值, Type为当序列化的原始类型。
  • customResponse(xxx)将会在实际调用序列化之前被调用, 并使用返回值的Object作为此次序列化的原始对象(也就是说允许用户在响应的序列化之前更改响应的对象)

同时, 序列化器实现本身具有优先级由getOrder()表示(值越低优先级越高)

当最终无法找到匹配的序列化器时, 会通过默认的优先级进行序列化(暂时仅响应序列化为此行为)。

@Bean
public HttpBodySerializer fastjsonBodySerializer() {
    return new FastJsonHttpBodySerializer() {
       @Override
       public int getOrder() {
           return 0;
       }
   };
 
@Bean
public HttpBodySerializer protoBufBodySerializer() {
    return new ProtoBufHttpBodySerializer() {
        @Override
        public int getOrder() {
            return 1;
        }
    };
}
   

多种序列化并存时的优先级

  • 请求序列化
  1. 如果指定了@RequestSerializer或者@Serializer中指定的序列化方式则使用该序列化方式
  2. 使用序列化协商中的参数指定的序列化器
  3. 根据ContentType查找序列化器
  4. 未找到则报错
  • 响应序列化
  1. 如果指定了@ResponseSerializer或者@Serializer中指定的序列化方式则使用该序列化方式
  2. 使用序列化协商中的参数指定的序列化器
  3. 根据Accept查找序列化器
  4. 根据RequestMapping中produces指定的序列化器(未找到进入6)
  5. 未找到使用默认优先级最高的序列化器

ProtoBuf序列化支持

Restlight内置了ProtoBuf序列化器的实现,对应支持的MediaType为application/x-protobuf(需要请求的Content-Typeapplication/x-protobuf

同时使用ProtoBuf序列化器序列化后的响应结果的Content-Type也为application/x-protobuf)。

针对ProtoBuf序列化的特点,ProtoBuf序列化后还将增加Header, X-Protobuf-SchemaX-Protobuf-Message分别返回Message对象的getDescriptorForType().getFile().getName()getDescriptorForType().getFullName的结果。

特殊类型返回值的序列化

不管Controller上是否加有@ResponseBody注解, 在使用序列化器序列化之前都将遵守以下原则

  • 值为String类型直接返回该字符串结果。
  • 值为byte[]类型直接将结果写入响应。
  • 值为ByteBuf类型时直接将结果写入响应
  • 以上均不符合则使用序列化器进行序列化。
  • 如果Controlelr上未配置@ResponseBody,值为基本类型或基本类型包装类将返回该类型的字符串结果(调用String.valueOf())。
  • 以上均不符合则抛异常。

4.10 - 请求参数聚合

支持将请求的参数聚合到Bean中

eg.

@GetMapping(value = "/test")
public String foo(@RequestBean Pojo Pojo) {
    return "";
}

private static class Pojo {

    @QueryParam("id")
    private int id;

    @HeaderParam("message")
    private String message;

    private AsyncRequest request;
    private AsyncResponse response;

    public int getId() {
        return id;
    }

    //getter & setter
}

4.11 - URL参数聚合

支持将Url中的参数与Form表单中的参数(仅当Content-Typeapplication/x-www-form-urlencoded时有效)聚合到Bean中。

eg:

请求 /test?id=1&msg=hello 中的id和message的值将绑定到pojo参数中

@GetMapping(value = "/test")
public String handle(@QueryBean Pojo Pojo) {
    return "";
}

private static class Pojo {

    private int id;

    @QueryBean.Name("msg")
    private String message;

    public int getId() {
        return id;
    }

    //getter & setter
}

4.12 - Context Path

Restlight支持全局Path,使用时需要做如下配置:

restlight.server.context-path=/global-path/

原始Controller方法为:

@Controller
@RequestMapping("/restlight/employee/")
public class EmployeeController {

    @GetMapping("/list")
    @ResponseBody
    public List<Employee> listAll() {
        List<Employee> employeeList = new ArrayList<>(16);

        employeeList.add(new Employee("LiMing", 25, "1403063"));
        employeeList.add(new Employee("LiSi", 36, "1403064"));
        employeeList.add(new Employee("WangWu", 31, "1403065"));

        return employeeList;
    }
}

使用全局Path后的请求路径为:/global-path/restlight/employee/list

4.13 - Aware扩展

Spring场景,Restlight支持通过xxxAware接口获取一些内部对象。

其中包含

  • RestlightBizExecutorAware: 获取业务线程池
  • RestlightIoExecutorAware: 获取IO线程池
  • RestlightServerAware: 获取RestlightServer
  • RestlightDeployContextAware: 获取DeployContext

eg.

获取业务线程池

@Controller
public class HelloController implements RestlightBizExecutorAware {

    private Executor bizExecutor;

    @Override
    public void setRestlightBizExecutor(Executor bizExecutor) {
        this.bizExecutor = bizExecutor;
    }


    @GetMapping("/foo")
    public CompletableFuture<String> foo() {
        return CompletableFuture.supplyAsync(() -> "Hello Restlight!", bizExecutor);
    }

}

4.14 - 路由缓存

Spring MVC路由的痛点

传统的Spring MVC中, 当我们的@RequestMapping注解中包含了复杂任何的复杂匹配逻辑(这里的复杂逻辑可以理解为除了一个url对应一个controller实现,并且url中没有*, ? . {foo}等模式匹配的内容)时方能在路由阶段有相对较好的效果,反之如通常情况下一个请求的到来到路由到对应的controller实现这个过程将会是在当前应用中的所有Controller中遍历匹配,值得注意的是通常在微服务提倡RestFul设计的大环境下一个这种遍历几乎是无法避免的, 同时由于匹配的条件本身的复杂性(比如说正则本身为人诟病的就是性能),因此伴随而来的则是SpringMVC的路由的损耗非常的大。

Restlight路由缓存

设计原则

  • 二八原则(80%的业务由20%的接口处理)
  • 算法:类LFU(Least Frequently Used)算法

我们虽然不能改变路由条件匹配本身的损耗, 但是我们希望能做尽量少的匹配次数来达到优化的效果。因此采用常用的"缓存"来作为优化的手段。 当开启了路由缓存后,默认情况下将使用类LFU(Least Frequently Used)算法的方式缓存十分之的Controller,根据二八原则(80%的业务由20%的接口处理),大部分的请求都将在缓存中匹配成功并返回(这里框架默认的缓存十分之一,是相对比较保守的设置)

算法逻辑

当每次请求匹配成功时,会进行命中纪录的加1操作,并统计命中纪录最高的20%(可配)的Controller加入缓存, 每次请求的到来都将先从缓存中查找匹配的Controller(大部分的请求都将在此阶段返回), 失败则进入正常匹配的逻辑。

什么时候更新缓存? 我们不会在每次请求命中的情况下都去更新缓存,因为这涉及到一次排序(或者m次遍历, m为需要缓存的Controller的个数,相当于挑选出命中最高的m个controller)。 取而代之的是我们会以概率的方式去重新计算并更新缓存, 根据2-8原则通常情况下我们当前缓存的内存就是我们需要的内容, 所以没必要每次有请求命中都去重新计算并更新缓存, 因此我们会在请求命中的一定概率条件下采取做此操作(默认0.1%, 称之为计算概率), 减小了并发损耗(这段逻辑本身基于CopyOnWrite, 并且为纯无锁并发编程,本身性能损耗就很低),同时此概率可配置可以根据具体的应用实际情况调整配置达到最优的效果。

配置建议

缓存比例:请求集中化比较高则设置更小(比如集中在1%的Controller上则可以设置为缓存1%) 计算率: 理论上设置的越高实时性越强(缓存更新频率越高)但是并发损耗也会升高,因此建议设置的相对小一些以应对激增的非常用请求即可。

4.15 - 快速失败

Restlight 支持根据请求任务的排队时间快速失败。具体地,从接收到首字节(TTFB)或请求任务进入线程池开始排队时开始计时, 如果请求任务真正执行时的时间与起始时间的差值大于指定值(timeout),那么直接结束当前请求(返回500)。

使用时,需要配置timeout与起始时间(首字节时间或者开始排队时间,默认后者),示例如下:

restlight.server.scheduling.timeout.BIZ.type=QUEUED
restlight.server.scheduling.timeout.BIZ.time-millis=30

restlight.server.scheduling.timeout.IO.type=TTFB
restlight.server.scheduling.timeout.IO.time-millis=30

其中,BIZIO为Scheduler的名称,type为开始计时的方式,默认为QUEUED, 表示从请求任务进入线程池排队时开始计时,TTFB表示从接收到首字节时开始计时,time-millis 表示超时时间。

4.16 - Mock测试

同Spring MVC一样,Restlight也提供了单元测试的功能,用于构造请求并将请求映射到对应的Handler,得到Handler的执行结果并测试。如果您对Spring-Test不熟悉,请参考Spring-Testing。由于Restlight与Spring-web天生存在冲突,因此MockMvc的使用方式与Spring Mvc的略有差异,详情如下文所示。需要注意的是:

  • 使用MockMvc及MockMvcBuilders时请正确引入restlight包下的,而不是spring-web测试包下的。
  • 由于Restlight暂不支持RestTemplate,因此与该功能有关的测试同样暂不支持,如@AutoConfigureWebClient、@WebMvcTest等。

使用该功能需要额外引入如下依赖:


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <version>${spring.boot.version}</version>
</dependency>

<dependency>
    <groupId>io.esastack</groupId>
    <artifactId>restlight-test-starter</artifactId>
    <scope>test</scope>
    <version>${restlight.version}</version>
</dependency>

1. 通过Context构造测试环境

该方式与Spring Mvc测试方式几乎无差异,需要注意的是:正确引入restlight包下的MockMvc及MockMvcBuilders。示例如下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class BootstrapWithContextTest {

    @Autowired
    private ApplicationContext context;

    private MockMvc mockMvc;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.contextSetup(context);
    }

    @Test
    public void testListAll() {
        mockMvc.perform(MockAsyncRequest.aMockRequest().withUri("/demo1/list").build())
                .addExpect(r -> assertTrue(((List) r.result()).isEmpty()))
                .addExpect(r -> assertEquals(200, r.response().status()));
    }

    @Test
    public void testListAll2() {
        mockMvc.perform(MockAsyncRequest.aMockRequest().withUri("/demo2/list").build())
                .addExpect(r -> assertTrue(((List) r.result()).isEmpty()))
                .addExpect(r -> assertEquals(200, r.response().status()));
    }
}

2. 通过Controller列表构造测试环境

该方式与Spring Mvc测试方式几乎无差异,需要注意的是:正确引入restlight包下的MockMvc及MockMvcBuilders。示例如下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class BootstrapWithSingletonTest {

    @Autowired
    private DemoController1 demoController1;

    @Autowired
    private DemoController2 demoController2;

    private MockMvc mockMvc;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.standaloneSetup(demoController1, demoController2).build();
    }

    @Test
    public void testListAll() {
        mockMvc.perform(MockAsyncRequest.aMockRequest().withUri("/demo1/list").build())
                .addExpect(r -> assertTrue(((List) r.result()).isEmpty()))
                .addExpect(r -> assertEquals(200, r.response().status()));
    }

    @Test
    public void testListAll2() {
        mockMvc.perform(MockAsyncRequest.aMockRequest().withUri("/demo2/list").build())
                .addExpect(r -> assertTrue(((List) r.result()).isEmpty()))
                .addExpect(r -> assertEquals(200, r.response().status()));
    }
}

3. 自动注入MockMvc

该方式与Spring Mvc测试方式几乎无差异,需要注意的是:正确引入restlight包下的MockMvc。示例如下:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class MockMvcAutowiredTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testListAll() {
        mockMvc.perform(MockAsyncRequest.aMockRequest().withUri("/demo1/list").build())
                .addExpect(r -> assertTrue(((List) r.result()).isEmpty()))
                .addExpect(r -> assertEquals(200, r.response().getStatus()));
    }

    @Test
    public void testListAll2() {
        mockMvc.perform(MockAsyncRequest.aMockRequest().withUri("/demo2/list").build())
                .addExpect(r -> assertTrue(((List) r.result()).isEmpty()))
                .addExpect(r -> assertEquals(200, r.response().getStatus()));
    }
}

4. 异步方法测试

与同步方法不同,如果原始Controller为异步方法,执行完perform()方法后直接对执行结果进行判断不会得到预期结果,因为原始Controller并未执行完,响应内容也尚未写入。因此如果原始Controller方法为异步,Restlight在执行完perform()方法后会阻塞等待异步方法执行完成,而后继续执行用户自定义的判断逻辑,使用时可以通过MockAsyncRequest的asynTimeout属性设置阻塞等待的时间(默认为-1)。

使用示例:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RestlightDemoApplication.class)
@AutoConfigureMockMvc
public class AsyncDemoControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testListAll() {
        mockMvc.perform(MockAsyncRequest.aMockRequest().withUri("/async/list").build())
                .addExpect(r -> assertTrue(((List) r.result()).isEmpty()))
                .addExpect(r -> assertEquals(200, r.response().status()));
    }
}

4.17 - 辅助配置

SpringBoot场景下大多数的配置可通过application.properties(或者yaml)配置文件即可完成配置,但是配置文件配置还是会有其缺陷

  • 无法动态配置(这里的动态指的是通过代码计算等方式决定配置)
  • 语法表达能力有限(比如ChannelOption无法通过配置文件表达)
  • 配置过多变得冗杂

等问题。

RestlightConfigure

用于支持SpringBoot场景显式配置

eg.

@Bean
public RestlightConfigure configure() {
    return restlight -> {
        restlight.address(8081)
                .childOption(ChannelOption.TCP_NODELAY, true)
                .channelHandler(new LoggingHandler())
                .addFilter((request, response, chain) -> {
                    // biz logic
                    return chain.doFilter(request, response);
                })
                .deployments()
                .addHandlerInterceptor(new HandlerInterceptor() {
                    @Override
                    public boolean preHandle(AsyncRequest request,
                                             AsyncResponse response,
                                             Object handler) {
                        // biz logic
                        return true;
                    }
                });
        restlight.options().setCoreBizThreads(16);
        restlight.options().setMaxBizThreads(32);
        // more...
    };
}

4.18 - 配置一览

Server相关配置

所有配置均以restlight.server开头, 基于properties的配置(yml以此类推)

配置项 默认 说明
host 0.0.0.0 服务绑定的ip
port 8080 服务绑定的端口
unix-domain-socket-file 不为空则使用Unix Domain Socket绑定到此文件(优先级高于ip:port的方式)
use-native-transports Linux环境下为true其余为false 是否使用原生epoll支持, 否则使用NIO的selector
connector-threads 1 连接线程池大小
io-threads cpu*2(默认不超过64) IO线程池大小
biz-termination-timeout-seconds 60 优雅停机等待超时时间
http2-enable false 是否开启Http2
compress false 是否启用HTTP响应压缩
decompress false 是否启用HTTP请求解压
max-content-length 4 * 1024 * 1024 最大contentLength限制(b)
max-initial-line-length 4096 最大request line限制(b)
max-header-size 8192 最大header size限制(b)
route.use-cached-routing true 开启路由缓存
route.compute-rate 1 路由计算率,取值范围0-1000,固定的概率之下更新路由
warm-up.enable false 是否开启服务预热功能
warm-up.delay 0 服务延迟暴露时间(单位:毫秒)
keep-alive-enable true false服务器将强制只支持短链接
soBacklog 128 对应netty的ChannelOption.SO_BACKLOG
write-buffer-high-water-mark -1 netty中channel的高水位值
write-buffer-low-water-mark -1 netty中channel的低水位值
idle-time-seconds 60 连接超时时间
logging 设置LoggingHandler用于打印连接及读写信息

核心功能配置

所有配置均以restlight.server开头, 基于properties的配置(yml以此类推)

配置项 默认 说明
context-path 全局path前缀
biz-threads.core cpu*4(默认在64-128之间) 业务线程池核心线程数
biz-threads.max cpu*6(默认在128-256之间) 业务线程池最大线程数
biz-threads.blocking-queue-length 512 业务线程池阻塞队列大小
biz-threads.keep-alive-time-seconds 180 业务线程池keepAliveTime 单位:秒
serialize.request.negotiation false 请求序列化协商
serialize.request.negotiation-param format 请求序列化协商参数名称
serialize.response.negotiation false 响应序列化协商
serialize.response.negotiation-param format 响应序列化协商参数名称
print-banner true 是否启动打印logo

SSL配置

所有配置均以restlight.server.ssl开头, 基于properties的配置(yml以此类推)

配置项 默认 说明
enable false 是否使用https
ciphers 支持的加密套件,不设置表示使用默认
enable-protocols 支持的加密协议,不设置表示使用默认
cert-chain-path 证书路径,https-enable为true时必须
key-path 私钥路径,https-enable为true时必须
key-password 私钥文件密钥(如果需要的话)
trust-certs-path Trust Store
session-timeout session过期时间, 0表示使用默认
session-cache-size session缓存大小, 0表示使用默认
handshake-timeout-millis SSL握手超时时间
client-auth 客户端认证类型,不设置默认无

5 - 扩展能力

Restlight内置了常用的Filter(IP白名单、新建连接数限制、CPU过载保护)、Interceptor(访问日志、参数签名验证)和表单参数解析器

可以一次引入所有扩展能力


<dependency>
	<groupId>io.esastack</groupId>
	<artifactId>restlight-ext-starter</artifactId>
	<version>${restlight.version}</version>
</dependency>

5.1 - 新建连接数限制

使用新建连接数限制时请先确保引入了依赖:

<dependency>
	<groupId>io.esastack</groupId>
	<artifactId>restlight-ext-filter-starter</artifactId>
	<version>${restlight.version}</version>
</dependency>

当前服务的新建连接数进行QPS限制。超过连接数限制的请求将被拒绝。

使用方式:

#开启新建连接数限制
restlight.server.ext.connection-creation-limit.enable=true
#设置每秒限制4000个新建连接,默认为20000
restlight.server.ext.connection-creation-limit.max-per-second=40000

5.2 - CPU Load保护

当服务宿主机Cpu负载达到一定阈值之后开始随机丢弃连接(新建连接, 已经建立的连接不受影响)

使用新建连接数限制时请先确保引入了依赖:

<dependency>
	<groupId>io.esastack</groupId>
	<artifactId>restlight-ext-filter-starter</artifactId>
	<version>${restlight.version}</version>
</dependency>
#开启Cpu Load自我保护
restlight.server.ext.cpu-load-protection.enable=true
#cpu负载阈值,默认为80.0D cpu超过此负载之后将开始随机丢弃连接
restlight.server.ext.cpu-load-protection.threshold=80.0D
#初始连接丢弃率,默认为10.0D(0代表0%, 100代表100%, 可以传小数)
restlight.server.ext.cpu-load-protection.initial-discard-rate=10.0D
#最大连接丢弃率,默认为80.0D(0代表0%, 100代表100%, 可以传小数)
restlight.server.ext.cpu-load-protection.max-discard-rate=80.0D

上面的配置将会在cpu负载到达75%时开始随机丢弃20%的新建连接, 随着cpu负载的升高达到100%则将会丢弃80%的连接。

说明:

当cpu负载到达或者超过cpu-load-threshold的值时开始丢弃连接,初始连接丢弃概率为initial-discard-rate, 随着cpu负载升高, 丢弃率将随着cpu负载的升高而成正比的升高, 当cpu负载达到100%时丢弃率将达到max-discard-rate.

5.3 - Access Log

使用Restlight访问日志拦截器时请确保已经引入了依赖:

<dependency>
	<groupId>io.esastack</groupId>
	<artifactId>restlight-ext-filter-starter</artifactId>
	<version>${restlight.version}</version>
</dependency>

Quick Start

AccessLog拦截器在每个请求结束后记录访问日志,内容包含:客户端地址、请求协议、请求url(不包含路径参数)、请求方法、请求耗时、响应状态码、响应body大小以及访问时间。使用时需要做如下配置:

#开启AccessLog
restlight.server.ext.accesslog.enable=true

配置

配置项 默认 说明
enable false 是否启用
directory logs 日志文件路径
fileName access.log 日志文件名
charset 日志编码
rolling true 是否按照时间滚动生成文件
date-pattern yyyy-MM-dd 日期滚动格式,yyyy-MM-dd表示按天为单位滚动,生成的文件名为access.yyyy-MM-dd.log, 仅支持按天和小时为单位滚动,因此可选值:yyyy-MM-dd或者yyyy-MM-dd_HH(注意不要使用yyyy-MM-dd HH, 生成的文件名可能不符合操作系统文件命名规范)
max-history 10 最大历史文件个数
full-uri false 是否打印uri中所有的内容(包含url参数)

5.4 - 跨域

引入依赖

<dependency>
	<groupId>io.esastack</groupId>
	<artifactId>restlight-ext-filter-starter</artifactId>
	<version>${restlight.version}</version>
</dependency>

开启跨域功能

restlight.server.ext.cors.enable=true

更多跨域相关配置

restlight.server.ext.cors.rules[0].anyOrigin=false
restlight.server.ext.cors.rules[0].origins=www.example.com,www.demo.com
restlight.server.ext.cors.rules[0].expose-headers=foo,bar
restlight.server.ext.cors.rules[0].allow-credentials=false
restlight.server.ext.cors.rules[0].allow-methods=GET,POST
restlight.server.ext.cors.rules[0].allow-headers=foo,bar
restlight.server.ext.cors.rules[0].max-age=3600

5.5 - 文件及表单参数解析

Restlight提供了表单参数解析的功能,使用时需要单独引入相应的包:

<dependency>
   <groupId>io.esastack</groupId>
   <artifactId>restlight-ext-multipart-starter</artifactId>
   <version>${restlight.version}</version>
</dependency>

文件参数

使用示例如下:

@Controller
@RequestMapping("/restlight/file/")
public class FileSupportController
    // 上传单个文件
    @RequestMapping("/upload")
    public String fileUpload(@UploadFile MultipartFile multipartFile) throws IOException {
        File temp = new File("D:\\" + multipartFile.originalFilename());
        multipartFile.transferTo(temp);
        return "SUCCESS";
    }
	
    // 上传一组文件
    @RequestMapping("/uploads")
    public String fileUploads(@UploadFile List<MultipartFile> files) throws IOException {
        for (MultipartFile file : files) {
            File temp = new File("D:\\" + file.originalFilename());
            file.transferTo(temp);
        }
        return "SUCCESS";
    }
}

Restlight默认请求body大小为4MB,当上传文件时需要根据需要调整该值的大小;默认的编码格式为:UTF-8。使用时,也可以通过配置文件改变上述参数值:

#设置请求body大小 4MB = 4 * 1024 * 1024 = 4194304
restlight.server.max-content-length=4194304

#编码方式
restlight.server.ext.multipart.charset=utf-8

#单个文件大小限制,默认-1(没有限制) 4KB = 4 * 1024 = 4096
restlight.server.ext.multipart.max-size=4096

#是否使用临时文件,为true时任何大小的文件都使用临时文件,默认为false
restlight.server.ext.multipart.use-disk=true

#临时文件目录
restlight.server.ext.multipart.temp-dir=D:\\temp

#当multipart-use-disk为false且单个文件大小超过该值时使用临时文件,默认2MB
restlight.server.ext.multipart.memory-threshold=2097152

非文件参数

在需要接收的方法参数上加上@FormParam注解,如下:

@RequestMapping("/upload")
public String uploadFormParams(@FormParam String formParam0, @FormParam String formParam1) {
    return formParam0 + "; " + formParam1;
}

5.6 - 签名认证

使用Restlight参数签名验证拦截器时请先引入依赖:

<dependency>
	<groupId>io.esastack</groupId>
	<artifactId>restlight-ext-interceptor-starter</artifactId>
	<version>${restlight.version}</version>
</dependency>

参数签名验证拦截器可以验证请求参数的签名,防止请求参数被篡改。使用时请做如下配置:

#开启参数签名验证功能的必需配置
restlight.server.ext.sign.enable=true

#调用方ID参数名,默认为appId
restlight.server.ext.sign.app-id-name=appId

#签名秘钥版本参数名,默认为sv
restlight.server.ext.sign.secret-version-name=sv

#请求时间戳参数名,默认为ts
restlight.server.ext.sign.timestamp-name=ts

#请求时间戳有效期:单位秒(默认为0)
restlight.server.ext.sign.expire-seconds=0

#签名参数名称,默认为sign
restlight.server.ext.sign.signature-name=sign

#是否对所有接口进行签名验证(默认为false)
restlight.server.ext.sign.verify-all=true

服务端验签详细过程

  • 第一步:从请求中获取签名值signature并去掉前后空格(先从url参数中获取,如果没有再从请求头中获取);
  • 第二步:从请求中获取时间戳timestamp并去掉前后空格(方式同上),如果配置了请求时间戳有效期,则判断是否在有效期内。
  • 第三步:从请求中获取appId、secretVersion并去掉前后空格(方式同上)。
  • 第四步:根据上述的appId、secretVersion、timestamp从自定义的SecretProvider获取secret。
  • 第五步:根据请求参数构建签名data[],具体步骤为:1.构建请求参数对应的paramData[]:获取所有的url参数(排除sign),按照参数名字典序升序排列,若一个参数对应多个值,则这多个值也按字典序升序排列(注意:所有参数名和参数值均会去掉前后空格)。如:http://api.xxx.com/getUserInfo?appId=your_appId&sv=1&ts=1555933697000&user_id=u001&sign=xxx&names=LiMing&names=ZhangSan对应的paramData[]=(“api_key=your_appId&names=LiMing&names=ZhangSan&sv=1&t=1555933697000&user_id=u001”).getBytes(“UTF-8”);2.构建请求body对应的bodyData[]:对于类型为POST且Content-Type不包含x-www-form-urlencoded的请求,直接通过request.getBody()获取bodyData[]。重要说明: 对于Content-Type包含x-www-form-urlencoded的POST请求,验证签名时会将body中参数合并到url的参数中一起处理,客户端加密时需要注意此种情况;3.合并paramData[]和bodyData[]作为签名data[]。
  • 第六步:使用HmacSha1算法生成data[]与secret的签名(详见esa-commons项目下SecurityUtils的getHmacSHA1方法)。
  • 第七步:验证signature与第六步生成的签名是否相等。

指定或排除需要进行签名验证的接口

Restlight提供了两种不同的方式来自定义需要进行签名验证的接口:1. 在全局接口都进行签名验证的情况下,使用@IgnoreSignValidation注解忽略指定接口的签名验证功能;2. 在全局接口都不进行签名验证的前提下,使用@SignValidation注解指定对需要进行签名验证的接口。默认使用方式2。 方式1使用示例:

restlight.server.ext.sign.verify-all=true
@RequestMapping("/index")
@IgnoreSignValidation
public void index() {
    TestService.list();
}

如上配置表示:对index()方法之外的其他接口均开启签名验证功能。

方式2使用示例:

restlight.server.ext.sign.verify-all=false
@RequestMapping("/index")
@SignValidation
public void index() {
    TestService.list();
}

如上配置表示:只对index()方法对应的接口开启签名验证功能。

自定义参数签名验证拦截器

Restlight默认使用HmacSHA1作为验签时原始请求的签名生成方法,当用户使用其它算法可以注入自定义的参数签名验证拦截器,使用示例如下:

@Component
public class CustomizeSignatureValidationFactory extends SignValidationHandlerInterceptorFactory {

    public CustomizeSignatureValidationFactory(SecretDistributor distributor) {
        super(distributor);
    }

    @Override
    protected AbstractSignatureRouteInterceptor doCreate(SignatureOptions options, SecretDistributor distributor) {
        return new AbstractSignatureRouteInterceptor(options, distributor) {
            @Override
            protected boolean validate(byte[] data, String signature, String sk) {
                // customize validation
            }
        };
    }
}

5.7 - XSS过滤

引入依赖:

<dependency>
	<groupId>io.esastack</groupId>
	<artifactId>restlight-ext-filter-starter</artifactId>
	<version>${restlight.version}</version>
</dependency>

使用方式:

#开启Xss过滤
restlight.server.ext.xss.enable=true
#Xss过滤模式,默认escape(转义模式),filter为过滤模式
restlight.server.ext.xss.mode=escape

配置好后自动对所有请求进行转义或者过滤。

XSS过滤范围

支持URL参数过滤及不完整Header过滤

Escape & Filter模式

Escape 模式

该模式会对用户请求的 URL参数 和 Header 进行转义,转义的字符集如下:

转义前 转义后
> &gt;
< &lt;
" &quot;
& &amp;

Filter 模式

该模式会对用户请求的 URL参数 和 Header 进行过滤,删除容易引起 Xss 的标签或者表达式,以空串代替,比如 name=<script>...</script> ,过滤以后会直接将 <script>...</script> 以空串替换,即 name="",需要以空串替换的标签和表达式如下:

标签或表达式
<script>…</script>
</script>
<script …>
src='…'
src="…"
eval(…)
e­xpression(…)
javascript:
alert
onload=
vbscript:

5.8 - IP白名单

IP白名单

使用Restlight内置的IP白名单时请先确保引入了依赖:

<dependency>
	<groupId>io.esastack</groupId>
	<artifactId>restlight-ext-filter-starter</artifactId>
	<version>${restlight.version}</version>
</dependency>

IP白名单拦截器可以过滤非法IP的访问。同时支持IP地址和正则表达式两种匹配方式,需要配置的内容如下:

#开启IP白名单拦截器的必需配置
restlight.server.ext.whitelist.enable=true

#IP白名单列表(多值请用逗号分隔,正则表达式regex:开头)
restlight.server.ext.whitelist.ips=10.10.1.1,regex:10.12.*

#缓存最近访问的IP地址(默认1024个)
restlight.server.ext.whitelist.cache-size=1024

#缓存的失效时间(单位:ms,默认为60s)
restlight.server.ext.whitelist.expire=60000

5.9 - 数据校验

Restlight集成了Hibernate Validator,提供了开箱即用的数据校验功能,通过注解完成对JavaBean、Controller方法参数和返回值的校验, 并支持异常消息国际化。

使用Restlight内置的数据校验请先确保引入了依赖:

<dependency>
	<groupId>io.esastack</groupId>
	<artifactId>restlight-ext-validator-starter</artifactId>
	<version>${restlight.version}</version>
</dependency>

普通JavaBean的校验

使用注解声明对属性的约束

private class Employee {
	@NotEmpty
	private String name;
	
	@Min(18)
	@Max(60)
	private int age;
	
	@Email
	private String email;
	
	@Length(min = 10, max = 20)
	private String address;

        // 级联校验
	@Valid
	private Object cascadingObject;
}

作为方法参数校验, 需要使用@Valid注解标记被校验的参数

@PostMapping("/add")
public String add(@Valid @RequestBody Employee employee) {
    return SUCCESS;
}

作为返回值, 需要使用@Valid注解标记方法或者参数

@Valid
@ResponseBody
@RequestMapping("/list")
public Employee list1() {
    return new Employee("", 16, "", "");
}

或者

@ResponseBody
@RequestMapping("/list")
public @Valid Employee list2() {
   return new Employee("", 16, "", "");
}

普通方法参数校验

直接使用注解

@RequestMapping("/update")
public String update(@RequestParam @NotEmpty String name, @RequestParam @Length(min = 10, max = 20) String newAddress) {
    return SUCCESS;
}

分组校验

使用@ValidGroup指定校验方法的参数、返回值校验时的分组。该注解只能标注在方法上并且value值只能为接口类(默认为Default.class)。

Example:

@ValidGroup(Interface.class)
@RequestMapping("/addGroup")
public String addGroup(@Valid @RequestBody Employee employee) {
    return SUCCESS;
}

自定义约束注解

当内置的约束注解不能满足业务需求时,可以使用@Constraint自定义约束注解,具体实现使用hibernate-validation,使用方式与Spring MVC无差异,示例如下:

自定义约束注解:

@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = LogInSuccess.LogInSuccessValidator.class)
@Target(value = {ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Documented
public @interface LogInSuccess {

    String message() default "登录校验未通过";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    class LogInSuccessValidator implements ConstraintValidator<LogInSuccess, String> {

        @Override
        public void initialize(LogInSuccess constraintAnnotation) {
            // Do nothing
        }

        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            return false;
        }
    }
}

使用自定义约束注解:

@RequestMapping("/getId")
public String getId(@RequestParam @LogInSuccess(message = "请先登录") String userName) {
    return "SUCCESS";
}

国际化

数据校验的异常消息允许自定义并且支持国际化,自定义异常处理消息的步骤如下:

自定义异常消息文件,在classpath路径下加入配置文件,如validation-message.properties

key1=value1
key2=value2
...

配置异常消息文件名,在application中配置异常消息文件名,如:

#该文件名称对应上面定义的validation-message.properties文件
restlight.server.ext.validation.message-file=validation-message

修改约束注解的message属性值,如:

@NotEmpty(message="{key1}")
public String name;

@Min(value=18, message="{key2}")
public int age;

针对不同语言定义不同的异常消息文件,如:

  • validation-message_zh_CN.properties
  • validation-message_en.properties
  • validation-message_cs.properties
  • validation-message_en.properties
  • ……

数据校验注解一览

注解 功能 说明
@AssertFalse 被注解元素必须为false
@AssertTrue 被注解的元素必须为true
@DecimalMax(value) 被注解的元素必须为一个数字,其值必须小于等于指定的最小值
@DecimalMin(Value) 被注解的元素必须为一个数字,其值必须大于等于指定的最小值
@Digits(integer=, fraction=) 被注解的元素必须为一个数字,其值必须在可接受的范围内
@Future 被注解的元素必须是未来的日期
@Max(value) 被注解的元素必须为一个数字,其值必须小于等于指定的最大值
@Min(value) 被注解的元素必须为一个数字,其值必须大于等于指定的最小值
@NotNull 被注解的元素必须不为null
@Null 被注解的元素必须为null
@Past 被注解的元素必须过去的日期
@Pattern 被注解的元素必须符合正则表达式
@Size(min=, max=) 被注解的元素必须在指定的范围(数据类型:String, Collection, Map and arrays)
@Email 被注解的元素被注释的元素必须是电子邮箱地址
@NotBlank 被注解的对象必须为字符串,不能为空,检查时会忽略空格
@NotEmpty 被注释的对象长度不能为0(数据:String,Collection,Map,arrays)
@Length(min=, max=) 被注解的对象必须是字符串并且长度必须在指定的范围内 Hibernate扩展注解
@Range(min=, max=) 被注释的元素必须在合适的范围内 (数据:BigDecimal, BigInteger, String, byte, short, int, long and 原始类型的包装类 ) Hibernate扩展注解
@URL(protocol=, host=, port=, regexp=, flags=) 被注解的对象必须是一个有效的URL,如果提供了protocol,host等,则该URL还需满足提供的条件 Hibernate扩展注解

6 - Spring Boot Actuator支持

Restlight适配了Spring Boot Actuator, 提供详细的健康检查以及监控等接口

Quick Start

引入Maven依赖:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
	<version>2.1.1.RELEASE</version>
</dependency>
<dependency>
	<groupId>io.esastack</groupId>
	<artifactId>restlight-starter-actuator</artifactId>
	<version>${restlight.version}</version>
</dependency>

访问 Get localhost:8080/actuator/info返回{}

6.1 - Restlight Endpoint扩展

Spring Boot Actuator基础之上Restlight提供额外的功能扩展

业务线程池Metrics

1、Json 格式

Get actuator/bizthreadpool

Example:

curl -X GET localhost:8080/actuator/bizthreadpool

返回

{
    "corePoolSize": 4,
    "maxPoolSize": 4,
    "queueLength": 1024,
    "keepAliveTimeSeconds": 180,
    "activeCount": 1,
    "poolSize": 4,
    "largestPoolSize": 4,
    "taskCount": 6,
    "queueCount": 0,
    "completedTaskCount": 5
}

2、Prometheus 格式

Get actuator/bizthreadpool4prometheus

Example:

curl -X GET localhost:8080/actuator/bizthreadpool4prometheus

返回

# HELP core_pool_size  
# TYPE core_pool_size gauge
core_pool_size 1.0
# HELP active_count  
# TYPE active_count gauge
active_count 0.0
# HELP queue_count  
# TYPE queue_count gauge
queue_count 0.0
# HELP largest_pool_size  
# TYPE largest_pool_size gauge
largest_pool_size 0.0
# HELP reject_task_count  
# TYPE reject_task_count gauge
reject_task_count -1.0
# HELP max_pool_size  
# TYPE max_pool_size gauge
max_pool_size 2.0
# HELP pool_size  
# TYPE pool_size gauge
pool_size 0.0
# HELP queue_length  
# TYPE queue_length gauge
queue_length 1024.0
# HELP keep_alive_time_seconds  
# TYPE keep_alive_time_seconds gauge
keep_alive_time_seconds 60.0
# HELP task_count  
# TYPE task_count gauge
task_count 0.0
# HELP completed_task_count  
# TYPE completed_task_count gauge
completed_task_count 0.0

业务线程池扩缩容

Post actuator/bizthreadpool

curl -X POST -H "Content-Type:application/json" -d "{\"corePoolSize\":\"1\",\"maxPoolSize\":\"2\"}" localhost:8080/actuator/bizthreadpool

IO线程池Metrics

1、Json 格式

Get actuator/ioexecutor

Example:

curl -X GET localhost:8080/actuator/ioexecutor

返回

{
    "childExecutors": [
        {
            "pendingTasks": 0,
            "maxPendingTasks": 2147483647,
            "ioRatio": 50,
            "taskQueueSize": 0,
            "tailTaskQueueSize": 0,
            "threadName": "Netty-I/O-1#0",
            "threadPriority": 5,
            "threadState": "RUNNABLE"
        },
        {
            "pendingTasks": 0,
            "maxPendingTasks": 2147483647,
            "ioRatio": 50,
            "taskQueueSize": 0,
            "tailTaskQueueSize": 0,
            "threadName": "Netty-I/O-1#1",
            "threadPriority": 5,
            "threadState": "RUNNABLE"
        }
    ],
    "threadCount": 2,
    "pendingTasks": 0,
    "threadStates": {
        "RUNNABLE": 2
    },
    "terminated": false,
    "shutDown": false
}

2、Prometheus 格式

Get actuator/ioexecutor4prometheus

Example:

curl -X GET localhost:8080/actuator/ioexecutor4prometheus

返回

# HELP pending_tasks_netty_io_1_0  
# TYPE pending_tasks_netty_io_1_0 gauge
pending_tasks_netty_io_1_0 0.0
# HELP thread_state_netty_io_1_1  
# TYPE thread_state_netty_io_1_1 gauge
thread_state_netty_io_1_1 1.0
# HELP thread_states_runnable  
# TYPE thread_states_runnable gauge
thread_states_runnable 2.0
# HELP task_queue_size_netty_io_1_1  
# TYPE task_queue_size_netty_io_1_1 gauge
task_queue_size_netty_io_1_1 0.0
# HELP thread_priority  
# TYPE thread_priority gauge
thread_priority 5.0
# HELP pending_tasks_netty_io_1_1  
# TYPE pending_tasks_netty_io_1_1 gauge
pending_tasks_netty_io_1_1 0.0
# HELP pending_tasks  
# TYPE pending_tasks gauge
pending_tasks 0.0
# HELP terminated  
# TYPE terminated gauge
terminated 0.0
# HELP max_pending_tasks  
# TYPE max_pending_tasks gauge
max_pending_tasks 2.147483647E9
# HELP thread_state_netty_io_1_0  
# TYPE thread_state_netty_io_1_0 gauge
thread_state_netty_io_1_0 1.0
# HELP io_ratio  
# TYPE io_ratio gauge
io_ratio 50.0
# HELP shutdown  
# TYPE shutdown gauge
shutdown 0.0
# HELP task_queue_size_netty_io_1_0  
# TYPE task_queue_size_netty_io_1_0 gauge
task_queue_size_netty_io_1_0 0.0
# HELP thread_count  
# TYPE thread_count gauge
thread_count 2.0

当使用的Springboot版本为2.3.X及以上时,需要在pom文件中引入micrometer-registry-prometheus 1.5.1及以上版本,此处以1.5.1版本为例,直接引入即可覆盖原有版本,具体操作如下:

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
    <version>1.5.1</version>
</dependency>

获取Restlight所有配置信息

Get actuator/restlightconfigs

Example:

curl -X GET localhost:8080/actuator/restlightconfigs

返回:

{
    "http2Enable": false,
    "useNativeTransports": false,
    "connectorThreads": 1,
    "ioThreads": 2,
    "coreBizThreads": 4,
    "maxBizThreads": 4,
    "blockingQueueLength": 1024,
    "keepAliveTimeSeconds": 180,
    "executorTerminationTimeoutSeconds": 60,
    "compress": false,
    "decompress": false,
    "maxContentLength": 4194304,
    "maxInitialLineLength": 4096,
    "maxHeaderSize": 8192,
    "soRcvbuf": 0,
    "soSendbuf": 0,
    "soBacklog": 128,
    "writeBufferHighWaterMark": 65536,
    "idleTimeSeconds": 60,
    "keepAliveEnable": true,
    "https": {
        "enable": false,
        "handshakeTimeoutMillis": 3000,
        "certificatePath": null,
        "privateKeyPath": null,
        "sessionTicketKeyPath": null,
        "sessionTimeoutSeconds": 0,
        "sessionCacheEnable": false,
        "sessionCacheSize": 0,
        "enabledCipherSuites": [],
        "enabledProtocols": []
    },
    "scheduling": {
        "defaultStrategy": "BIZ",
        "bufferSize": 4096,
        "batchingSize": 50
    },
    "route": {
        "useCachedRouting": true,
        "cacheRatio": 10,
        "computeRate": 1
    },
    "contextPath": null,
    "validationMessageFile": null,
    "serialize": {
        "request": {
            "negotiation": false,
            "negotiationParam": "format"
        },
        "response": {
            "negotiation": false,
            "negotiationParam": "format"
        }
    },
    "ext": {},
    "host": null,
    "port": 8080,
    "unixDomainSocketFile": null,
    "printBanner": true,
    "warmUp": {
        "enable": false,
        "delay": 0
    }
}

强制Full GC

Post actuator/forcefgc

Example:

curl -X POST localhost:8080/actuator/forcefgc

修改优雅停机等待时间

Post actuator/terminationtimeout

Example:

curl -X POST -H "Content-Type:application/json" -d "{\"timeout\": 120}" localhost:8080/actuator/terminationtimeout

返回

Success

6.2 - 自定义Endpoint

用户可以自己定义Endpoint实现定制化的健康检查接口

eg

@Endpoint(id = "appId")
public class AppIdEndpoint {

    @ReadOperation
    public String appId() {
        return "esa-restlight";
    }
}

上面的代码自定义了一个Endpoint接口并返回appid

将上面接口注入Spring容器

@Bean
public AppIdEndpoint endpoint() {
    return new AppIdEndpoint();
}

启动之后访问curl -X GET localhost:8080/actuator/appId

返回

esa-restlight

自定义异步EndPoint

用户可以自己定义基于CompletableftureEndpoint实现定制化的健康检查接口

eg

@Endpoint(id = "appId")
public class AppIdEndpoint {

    @ReadOperation
    public CompletableFuture<String> appId() {
        return CompletableFuture.supplyAsync(() -> {

            // do something...

            return "esa-restlight";
        });
    }
}

上面的代码自定义了一个异步的Endpoint接口并返回appid

将上面接口注入Spring容器

@Bean
public AppIdEndpoint endpoint() {
    return new AppIdEndpoint();
}

启动之后访问curl -X GET localhost:8080/actuator/appId

返回

esa-restlight

6.3 - 使用独立端口

默认情况下健康检查的接口都将与Restight使用同一个HttpServer服务, 如果需要将健康检查接口与业务接口分别使用不同的端口则需要添加自定义配置

详细配置如下:

#配置健康检查暴露的端口
management.server.port=8081

启动后看到日志打印

Started Restlight(Actuator) server in 386 millis on port:8081

辅助配置

SpringBoot场景下大多数的配置可通过application.properties(或者yaml)配置文件即可完成配置,但是配置文件配置还是会有其缺陷

  • 无法动态配置(这里的动态指的是通过代码计算等方式决定配置)
  • 语法表达能力有限
  • 配置过多变得冗杂

等问题。

ManagementConfigure

用于支持SpringBoot场景显式配置

eg.

@Bean
public ManagementConfigure configure() {
    return restlight -> {
        restlight.address(8081)
                .addFilter((request, response, chain) -> {
                    // biz logic
                    return chain.doFilter(request, response);
                });
        restlight.options().setCoreBizThreads(16);
        restlight.options().setMaxBizThreads(32);
        // more...
    };
}

7 - Spring MVC 支持

Restlight支持了Spring MVC的使用习惯,你可以按照Spring MVC的方式使用Restlight

引入依赖

<dependency>
	<groupId>io.esastack</groupId>
	<artifactId>restlight-springmvc-provider</artifactId>
	<version>${restlight.version}</version>
</dependency>

7.1 - Spring MVC 注解支持

支持的注解

  • @Controller
  • @RestController
  • @RequestMapping
  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • @PatchMapping
  • @ResponseStatus
  • @ControllerAdvice
  • @ExceptionHandler
  • @RequestParam
  • @RequestHeader
  • @PathVariable
  • @CookieValue
  • @MatrixVariable
  • @RequestAttribute

注解能力扩展

@RequestParam, @ReqeustHeader,@PathVariable,@CookieValue, @MatrixVariable,@RequestAttribute

参数绑定相关注解除SpringMVC用法外,同时支持

Constructor接收一个String类型的参数

eg.

public void foo(@RequestParam User user) {
    //...
}

static class User {
    final String name;
    public User(String str) {
        this.name = name;
    }
}

存在静态的valueOf()或者fromString()方法

eg.

public void foo(@RequestParam User user, @RequestHeader Car car) {
    //...
}

static class User {
    String name;
    
    public static User valueOf(String str) {
        User user = new User();
        user.name = name;
        return user;
    }
}

static class Car {
    String name;
    
    public static Car fromString(String str) {
        Car car = new Car();
        car.name = name;
        return car;
    }
}

个别注解说明

@RequestParam

除普通用法外, 当未指定value()或者name且参数对象为Map<String, List<String>类型时, 将整个ParameterMap(即AsyncRequest.getParameterMap)作为参数的值。

eg.

public void foo(@RequestParam Map<String, List<String> params) {
    //...
}

@CookieValue

普通String类型

public void foo(@CookieValue String c) {
    //...
}

Cookie对象(io.netty.handler.codec.http.cookie.Cookie)

public void foo(@CookieValue Cookie c) {
    //...
}

获取所有的Cookie

public void foo(@CookieValue Set<Cookie> cookies) {
    //...
}

@RequestHeader

除获取单个header之外, 可以如果参数类型为io.netty.handler.codec.http.HttpHeaders则以所有的Header作为参数的值

eg:

public void foo(@RequestHeader HttpHeaders headers) {
    //...
}

7.2 - ExceptionHandler支持

Restlight支持业务自定义异常处理逻辑。对于Controller方法中抛出的异常,处理逻辑和顺序如下(注意:一个异常只会被处理一次):

1.尝试在Controller内部查找异常处理方法来处理当前异常,未找到进入2 2.尝试寻找全局异常处理方法来处理当前异常,未找到则返回错误信息

Controller级异常处理

局部异常处理方法只能处理当前Controller中抛出的异常,示例如下:

@RestController
@RequestMapping("/exception")
public class LocalExceptionResolver {

    @RequestMapping("/willBeHandled")
    public void willBeHandled() {
        throw new IllegalArgumentException("IllegalArgumentException...");
    }

    @RequestMapping("/willNotBeHandled")
    public void willNotBeHandled() {
        throw new RuntimeException("RuntimeException...");
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public String handleLocalException(IllegalArgumentException ex) {
        return "HandleLocalException [" + ex.getMessage() + "]";
    }
}

handleLocalException(XXX)方法会处理当前类中所有的IllegalArgumentException异常。

@ControllerAdvice全局异常处理

通过@ControllerAdvice@RestControllerAdvice来标识全局异常处理类,并可通过相应的属性设置该全局异常处理类生效的范围。使用示例如下:

@RestControllerAdvice(basePackages = {"esa.restlight.samples.starter.exceptionhandler", "esa.restlight.samples.starter.controller"})
public class GlobalExceptionResolver {

    @ExceptionHandler(Exception.class)
    public Result handleException(Exception ex) {
        return Result.ok();
    }

    static class Result {
        private int status;
        private String msg;

        static Result ok() {
            Result r = new Result();
            r.status = 200;
            return r;
        }
        
        // getter and setter
    }

}

如上所示, handleException(XXX)方法会处理esa.restlight.samples.starter.exceptionhandleresa.restlight.samples.starter.controller包中抛出的所有Exception

高级使用

为了让用户更方便的使用异常处理, 类似的我们支持像Controller上一样的去使用一些注解的功能如 @RequestParam, @RequestHeader@CookieValue等方便的进行参数绑定而不是自己手动的去Request里面取并做一次参数转换的操作。

eg:

@ControllerAdvice
public class GlobalExceptionResolver {

    @ExceptionHandler(Exception.class)
    public String handleException(@RequestParam("foo") String foo, Exception ex) {
        // do something here...
        return "HandleGlobalException [" + ex.getMessage() + "]";
    }

}

异步异常处理器

异常处理器可以类似Controller中异步处理一样完成异步的处理 Restlight异步支持

  • CompletableFuture
  • ListenableFuture(Guava)
  • Future(io.netty.util.concurrent.Future)

使用CompletableFuture异步方式:

@ExceptionHanlder(Exception.class)
public CompletableFuture<String> handleExcption(Exception ex) {
    return CompletableFuture.supplyAsync(() -> {
        // ...
        return "Hello Restlight!";
    });
}

ListenableFuture(Guava)Future(io.netty.util.concurrent.Future)同理。

8 - JAX-RS 支持

Restlight支持了JAX-RS的使用习惯,你可以按照JAX-RS的方式使用Restlight

引入依赖

<dependency>
	<groupId>io.esastack</groupId>
	<artifactId>restlight-jaxrs-provider</artifactId>
	<version>${restlight.version}</version>
</dependency>

8.1 - JAX-RS 注解支持

支持的注解

  • @Path
  • @GET
  • @POST
  • @PUT
  • @DELETE
  • @HEAD
  • @PATCH
  • @OPTIONS
  • @Consumes
  • @Produces
  • @QueryParam
  • @PathParam
  • @HeaderParam
  • @MatrixParam
  • @CookieParam
  • @FormParam
  • @BeanParam
  • @DefaultValue

注解使用

@QueryParam, @PathParam,@HeaderParam,@MatrixParam, @MatrixVariable,@FormParam

参数绑不支持javax.ws.rs.ext.ParamConverterProvier扩展

个别注解说明

@QueryParam

除普通用法外, 当未指定value()或者name且参数对象为Map<String, List<String>类型时, 将整个ParameterMap(即AsyncRequest.getParameterMap)作为参数的值。

eg.

public void foo(@QueryParam Map<String, List<String> params) {
    //...
}

@CookieParam

普通String类型

public void foo(@CookieParam String c) {
    //...
}

Cookie对象(io.netty.handler.codec.http.cookie.Cookie)

public void foo(@CookieParam Cookie c) {
    //...
}

获取所有的Cookie

public void foo(@CookieParam Set<Cookie> cookies) {
    //...
}

@HeaderParam

除获取单个header之外, 可以如果参数类型为io.netty.handler.codec.http.HttpHeaders则以所有的Header作为参数的值

eg:

public void foo(@HeaderParam HttpHeaders headers) {
    //...
}

9 - Restlight Server

Restlight Server

esa.restlight.server.RestliteRestlight架构中的Restlight Server模块的入口类, 在ESA HttpServer 基础上丰富了更多的功能

  • 引入业务线程池
  • 基于CompletableFuture的响应式编程支持
  • 线程调度
  • Filter
  • 请求路由(根据url, method, header等条件将请求路由到对应的Handler)

9.1 - Quick Start

引入依赖

<dependency>
	<groupId>io.esastack</groupId>
	<artifactId>restlight-server</artifactId>
	<version>${restlight.version}</version>
</dependency>

一行代码启动一个Http Server

Restlite.forServer()
        .daemon(false)
        .deployments()
        .addRoute(route(get("/hello"))
                .handle((request, response) ->
                        response.sendResult("Hello Restlight!".getBytes(StandardCharsets.UTF_8))))
        .server()
        .start();

运行并访问: http://localhost:8080/hello 即可看到输出:

Hello Restlight!

9.2 - 术语

Mapping

表示一个请求匹配的条件, 用于确定一个请求是否能够路由到某个目标对对象。

eg.

curl -X GET http://localhost:8080/hello

Mapping.get("/hello");

curl -X POST http://localhost:8080/foo?a=1

Mapping.post("/foo")
    .hasParam("a", "1");

curl -X GET -H "a:1" -H "Content-Type:application/json" -H "Accept:application/json" http://localhost:8080/foo?a=1

Mapping.get("/foo")
        .hasParam("a", "1")
        .noneParam("b", "1")
        .hasHeader("a", "1")
        .noneHeader("b", "1")
        .consumes(MediaType.APPLICATION_JSON)
        .produces(MediaType.APPLICATION_JSON);

Route

Route中包含了一个Mapping用于路由匹配, 一个请求都将期望路由到具体的一个Route, 如果找不到任何一个Route则响应一个404, 同时一个Route还负责请求本身的业务处理。

eg.

Mapping mapping = Mapping.get("/foo");
Route route = Route.route(mapping)
        .handle((request, response) -> {
            response.sendResult("Hello Restlight!".getBytes(StandardCharsets.UTF_8));
        })
        .onError(((request, response, error) -> {
            // error occurred
        }))
        .onComplete(((request, response, t) -> {
            // request completed
        }));

9.3 - 请求处理

业务处理

Mapping mapping = Mapping.get("/foo");
Route route = Route.route(mapping)
        .handle((request, response) -> {
            // biz logic here
            response.sendResult("Hello Restlight!".getBytes(StandardCharsets.UTF_8));
        });

异常处理

Mapping mapping = Mapping.get("/foo");
Route route = Route.route(mapping)
        .handle((request, response) -> {
            // biz logic here
            response.sendResult("Hello Restlight!".getBytes(StandardCharsets.UTF_8));
        })
        .onError(((request, response, error) -> {
            // error occurred
        }));

Complete事件

Mapping mapping = Mapping.get("/foo");
Route route = Route.route(mapping)
        .handle((request, response) -> {
            // biz logic here
            response.sendResult("Hello Restlight!".getBytes(StandardCharsets.UTF_8));
        })
        .onComplete(((request, response, t) -> {
            // request completed
        }));

异步

Route请求处理生命周期均支持基于Completablefuture的异步使用

Route route = route(get("/foo"))
        .handleAsync((request, response) ->
                CompletableFuture.runAsync(() -> {
                    // biz logic
                }))
        .onErrorAsync((request, response, throwable) ->
                CompletableFuture.runAsync(() -> {
                    // error
                }))
        .onCompleteAsync((request, response, t) ->
                CompletableFuture.runAsync(() -> {
                    // complete
                }));

10 - Restlight Core

esa.restlight.core.RestlightRestlight架构中的Restlight Core模块的入口类, 在Restlight Server 基础上丰富了更多的功能
  • Controller
  • ControllerAdvice
  • HandlerInterceptor: 拦截器
  • ExceptionHandler: 全局异常处理器
  • BeanValidation: 参数校验
  • ArgumentResolver: 参数解析扩展
  • ArgumentResolverAdvice: 参数解析扩展
  • ReturnValueResolver: 返回值解析扩展
  • ReturnValueResolverAdvice: 返回值解析扩展
  • RequestSerializer: 请求序列化器(通常负责反序列化Body内容)
  • ResposneSerializer: 响应序列化器(通常负责序列化响应对象到Body)
  • 内置Jackson, Fastjson, Gson, ProtoBuf序列化支持

Restlight CoreRestlight ServerRoute的业务处理部分做了封装, 完成拦截器,参数绑定,反序列化,返回值解析,序列化等一系列功能。

Restlight Core为核心实现, 实际使用时需配合Restlight SpringMVC以及Restlight JAX-RS实现。

由于Restlight Core为标准实现, 需要配合Restlight SpringMVC或者Restlight JAX-RS一起使用

10.1 - 配合 JAX-RS标准

基于Restlight Core为兼容JAX-RS注解使用习惯的扩展实现

eg.

引入依赖

<dependency>
	<groupId>io.esastack</groupId>
	<artifactId>restlight-core</artifactId>
	<version>${restlight.version}</version>
</dependency>
<dependency>
	<groupId>io.esastack</groupId>
	<artifactId>restlight-jaxrs-provider</artifactId>
	<version>${restlight.version}</version>
</dependency>

编写Controller

@Path("/hello")
public class HelloController {

    @Path("/restlight")
    @GET
    @Produces(MediaType.TEXT_PLAIN_VALUE)
    public String restlight() {
        return "Hello Restlight!";
    }
}

使用Restlight启动Server

Restlight.forServer()
        .daemon(false)
        .deployments()
        .addController(HelloController.class)
        .server()
        .start();

启动并访问: http://localhost:8080/hello 即可看到输出:

Hello Restlight!

10.2 - 配合 SpringMVC标准

基于Restlight Core为兼容SpringMVC注解使用习惯的扩展实现

eg.

引入依赖

<dependency>
	<groupId>io.esastack</groupId>
	<artifactId>restlight-core</artifactId>
	<version>${restlight.version}</version>
</dependency>
<dependency>
	<groupId>io.esastack</groupId>
	<artifactId>restlight-springmvc-provider</artifactId>
	<version>${restlight.version}</version>
</dependency>

编写Controller

@RequestMapping("/hello")
public class HelloController {

    @GetMapping(value = "/restlight")
    public String restlight() {
        return "Hello Restlight!";
    }
}

使用Restlight启动Server

Restlight.forServer()
        .daemon(false)
        .deployments()
        .addController(HelloController.class)
        .server()
        .start();

启动并访问: http://localhost:8080/hello 即可看到输出:

Hello Restlight!

11 - Restlight for Spring

esa.restlight.spring.Restlight4SpringRestlight架构中的Restlight for Srping模块的入口类, 在Restlight Core 基础上增强了自动配置功能

  • Route自动配置
  • Filter自动配置
  • Controller自动配置
  • ControllerAdvice自动配置
  • HandlerInterceptor自动配置
  • ExceptionHandler自动配置
  • ArgumentResolver自动配置
  • ArgumentResolverAdvice自动配置
  • ReturnValueResolver自动配置
  • ReturnValueResolverAdvice自动配置
  • RequestSerializer自动配置
  • ResposneSerializer自动配置
  • Validator自动配置

11.1 - Quick Start

引入依赖

<dependency>
	<groupId>io.esastack</groupId>
	<artifactId>restlight-spring</artifactId>
	<version>${restlight.version}</version>
</dependency>

<dependency>
	<groupId>io.esastack</groupId>
	<artifactId>restlight-springmvc-provider</artifactId>
	<version>${restlight.version}</version>
</dependency>

编写Controller并注入Spring

// Spring容器扫描并注入
@Controller
@RequestMapping("/hello")
public class HelloController {

    @GetMapping(value = "/restlight")
    public String restlight() {
        return "Hello Restlight!";
    }
}

使用Restlight4Spring启动Server

ApplicationContext context = ...
Restlight4Spring.forServer(context)
        .daemon(false)
        .server()
        .start();

启动并访问: http://localhost:8080/hello 即可看到输出:

Hello Restlight!

12 - 全链路异步

当前Restlight版本支持FilterHandlerInterceptor, Controller, ExceptionHandler异步。

对应处理链路

restlightexecution.png

上述对应着一个请求的完成执行链路, 并且每个链路都支持异步。

12.1 - Restlight 异步保证

Restlight保证在框架层面对所有的CompletableFuture处理都不会主动的切换执行的线程

这个保证使得用户无需担心不知道自己的代码将会在什么线程上执行。

用户在HandlerInterceptor中写下了如下代码

private final Executor customExecutor = ...;
            
@Override
public CompletableFuture<Boolean> preHandle0(AsyncRequest request,
                                             AsyncResponse response,
                                             Object handler) {
    // 线程切换到 customExecutor并回调Restlight
    return CompletableFuture.supplyAsync(() -> {
        // ....
        return true;
    }, customExecutor);
}

其中当执行HandlerInterceptor.preHandle(xxx)时用户使用了自定义的线程池作为异步实现,并在完成操作后回调Restlight, 后续所有Controller, ExceptionHandler等操作都将在customExecutor的线程上执行(除非用户主动切换)

下面的Controller将会在customExecutor的线程上被调用, 而不是业务线程池

@GetMapping(value = "/test")
public String foo() {
    // biz logic
    return "Hello Restlight!";
}

如果需要回到业务线程池执行则需要用户自行通过CompletableFuture进行操作

@GetMapping(value = "/test")
public CompletableFuture<String> foo() {
    // 回到业务线程池执行Controller逻辑
    return CompletableFuture.supplyAsync(() -> {
        // biz logic
        return  "Hello Restlight!";
    }, bizExecutor);
}

12.2 - CompletableFuture使用注意

  • CompletableFuture不带xxxAsync(xxx)的方法默认会在当前线程执行(当前线程指的是调用CompletableFuture.complete(xxx)或者CompletableFuture.completeExceptionally(xxx)的线程), 因此此种方式CompletableFuture的线程执行策略为尽量不切换线程(这里的尽量并不是完全一定, 因为如果当前future已经为完成的状态那么CompletableFuture会直接执行后续逻辑)
  • CompletableFuturexxxAsync(xxx)的方法要求传入一个Executor, 后续回掉逻辑将在这个Executor中执行。 默认不传的情况下会使用ForkJoinPool中的公共线程池。 因此应当对所有的xxxAsync(xxx)方法调用格外注意, 一旦使用了错误的线程池可能导致隔离性的缺失, 性能不符合预期等问题。

13 - 使用注意

13.1 - Controller 语义不能重复

出于性能考虑,Restlight中的所有RequestMapping的path属性不允许有语义上的重复(除非业务能容忍这个问题), 如果存在语义上的重复Restlight既不会在启动时报错提示,也不保证真正调用时的稳定性

  • 错误示例1:
@RequestMapping("/foo")
public void foo() {
    // ...
}

@PostMapping("/foo")
public void foo1() {
    // ...
}
  • 错误示例2:
@RequestMapping("/foo/{bar}")
public void foo(@PathVariable String bar) {
    // ...
}

@PostMapping("/foo/bar")
public void foo1() {
    // ...
}

Restlight检测到歧义时会打印WARNING日志

13.2 - 大文件上传

当前版本Restlight会将收到的文件存在内存之中,请勿上传超大文件。