This is the multi-page printable view of this section. Click here to print.
Restlight Starter
Restlight Starter
在Restlight Spring
基础上封装的Spring Boot Starter
提供基于Spring Boot
的自动配置- 1: Thread-Scheduling
- 2: Filter
- 3: 拦截器
- 4: 异常处理
- 5: 参数解析
- 6: ArgumentResolverAdvice
- 7: 返回值解析
- 8: ReturnValueResolverAdvice
- 9: 序列化
- 10: 请求参数聚合
- 11: URL参数聚合
- 12: Context Path
- 13: Aware扩展
- 14: 路由缓存
- 15: 快速失败
- 16: Mock测试
- 17: 辅助配置
- 18: 配置一览
1 - Thread-Scheduling
线程调度允许用户根据需要随意制定Controller
在IO
线程上执行还是在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 "";
}
Tip
- 自定义
Scheduler
时请勿使用IO
,BIZ
,Restlight
等作为name(作为Restlight
中Scheduler
的保留字) - 定义线程池建议使用
RestlightThreadFactory
以获得更高的性能
Warning
基于ThreadPoolExecutor
类型自定义Scheduler
时, 不管是否设置RejectExecutionHandler
,Restlight
都会覆盖ThreadPoolExecutor
中的RejectExecutionHandler
, 即不允许用户自定义实现RejectExecutionHandler
实现拒绝策略(因为Restlight
需要保证每个请求都能被正确的完成,否则可能会导致链接等资源无法被释放等问题), 相反如果自定义实现Scheduler
时请保证每个请求都被正确的完成。
配置
所有配置均以restlight.server.scheduling
开头
配置项 | 默认 | 说明 |
---|---|---|
default-scheduler | BIZ | 在不加@Scheduled 注解时采用的Scheduler |
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
继续执行后续操作。
Tip
上面演示的异步只是开了一个新的线程, 实际场景中可使用Netty
等实现更优雅的异步方式。
终止Filter
的执行
当不期望执行后续的Filter
时可返回一个CompletableFuture.completedFuture(null)
实例。
@Override
public CompletableFuture<Void> doFilter(AsyncRequest request, AsyncResponse response, FilterChain chain) {
return CompletableFuture.completedFuture(null);
}
Warning
doFilter(xxx)
请勿返回null
- 所有方法都将会在IO线程上调用,尽量不要阻塞, 否则将对性能会有较大的影响。
3 - 拦截器
Restlight
支持多种拦截器,适用于不同性能/功能场景
- RouteInterceptor
- MappingInterceptor
- HandlerInterceptor
- InterceptorFactory
Tip
实现对应的拦截器接口并注入Spring即可Note
在Interceptor#preHandle0()和Interceptor#postHandle0()中直接通过throw抛出的异常将不会被 异常处理器处理,如果需要被正常处理,请使用CompletableFuture.completeExceptionally()封装异常拦截器定位
面向Controller/Route
, 同时支持按需匹配的拦截器
- 面向
Controller/Route
: 拦截器一定能让用户根据需求选择匹配到哪个Controller
接口 - 按需匹配: 支持按照
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
一致,但那只是巧合)。
Note
- 框架在真正执行拦截器调用的时候只会调用上述方法(而不是
preHandle(xxx)
,postHandle(xxx)
,afterCompletion(xxx)
) - 请勿直接使用此接口,此接口仅仅是拦截器的核心实现类
拦截器匹配
初始化阶段: 初始化阶段为每一个Controller
确定所有可能匹配到当前Controller
的拦截器列表(包含可能匹配以及一定会匹配到当前Controller
的拦截器)。
运行时阶段: 一个请求AsyncRequest
到来时将通过将AsyncRequest
作为参数传递到拦截器做路由判定, 决定是否匹配。
Tip
通过初始化与运行时两个阶段的匹配行为扩展满足用户多样性匹配需求, 用户既可以直接将拦截器绑定到固定的Controller
也可以让拦截器随心所欲的根据请求去选择匹配。
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
接口为同时拥有Affinity
(Controller/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;
}
};
}
Note
运行时仅在request.containsHeader("X-Foo")
上做匹配, 性能损耗极低。
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);
}
Tip
运行时无性能损耗, 用于需要将拦截器固定的绑定到Controller
的场景, 这里的boolean match(xx)
方法返回的true
和false
实际上相当于Affinity
接口返回Affinity.ATTACHED
以及Affinity.DETACHED
(即只有一定匹配和一定不匹配)
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;
}
}
Warning
涉及到正则匹配, 会有较大性能损失, 且仅支持URI匹配。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 - 异常处理
Spring MVC异常处理
Restlight for Spring MVC支持使用Spring MVC中的@ExceptionHandler
, @ControllerAdvice
等注解,并且对此能力进行了增强
参考ExceptionHandler支持
Note
仅在项目引入了restlight-springmvc-provider
依赖的情况下使用(默认引入)
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);
}
}
Tip
处理不同异常类型实现不同的ExceptionResolver<T extends Throwable>
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;
}
}
初始化逻辑:
- 按照
getOrder()
方法的返回将Spring容器中所有的ArgumentResolver
进行排序 - 按照排序后的顺序依次调用
supports(Param param)
方法, 返回true
则将其作为该参数的ArgumentResolver
, 运行时每次请求都将调用resolve(AsyncRequest request, AsyncResponse response)
方法进行参数解析, 并且运行时不会改变。 - 未找到则启动报错。
细心的人可能会发现该设计可能并不能覆盖到以下场景
- 因为
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
类似
初始化逻辑:
- 按照
getOrder()
方法的返回将所有的ArgumentResolverFactory
进行排序 - 按照排序后的顺序依次调用
supports(Param param)
方法, 返回true
则将其作为该参数的ArgumentResolverFactory
, 同时调用createResolver(Param param, List<? extends HttpRequestSerializer> serializers)
方法创建出对应的ArgumentResolver
- 未找到则启动报错。
由于初始化时通过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的功能
Note
自定义ArgumentResolverAdapter
比框架自带的优先级高(如@RequestHeader
,@RequestParam
等), 如果匹配上了自定义实现,框架默认的功能在当前参数上将不生效。
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;
}
Note
自定义ArgumentResolverFactory
比框架自带的优先级高(如@RequestHeader
,@RequestParam
等), 如果匹配上了自定义实现,框架默认的功能在当前参数上将不生效。
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
自定应时同样需要实现ArgumentResolverAdviceAdapter
或ArgumentResolverAdviceFactory
接口
方式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
接口的使用方式相同, 这里不过多赘述。
Note
ArgumentResolverAdvice
与ArgumentResolver
生命周期是相同的, 即应用初始化的时候便会决定每个参数的ArgumentResolverAdvice
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;
}
}
初始化逻辑:
- 按照
getOrder()
方法的返回将Srping容器中所有的ReturnValueResolverAdapter
进行排序 - 按照排序后的顺序依次调用
supports(InvocableMethod invocableMethod)
方法, 返回true
则将其作为该参数的ReturnValueResolver
, 运行时每次请求都将调用resolve(Object returnValue, AsyncRequest request, AsyncReponse response)
方法进行参数解析, 并且运行时不会改变。 - 未找到则启动报错。
细心的人可能会发现该设计可能并不能覆盖到以下场景
- 因为
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
类似
初始化逻辑:
- 按照
getOrder()
方法的返回将所有的ReturnValueResolverFactory
进行排序 - 按照排序后的顺序依次调用
supports(InvocableMethod invocableMethod)
方法, 返回true
则将其作为该参数的ReturnValueResolverFactory
, 同时调用createResolver(InvocableMethod invocableMethod, List<? extends HttpResponseSerializer> serializers)
方法创建出对应的ReturnValueResolver
- 未找到则启动报错。
由于初始化时通过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的功能
Note
自定义ReturnValueResolverAdapter
比框架自带的优先级高(如@ResponseBody
), 如果匹配上了自定义实现,框架默认的功能在当前方法上上将不生效。
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";
}
Note
自定义ReturnValueResolverFactory
比框架自带的优先级高(如@ResponseBody
), 如果匹配上了自定义实现,框架默认的功能在当前方法上上将不生效。
8 - ReturnValueResolverAdvice
ReturnValueResolverAdvice
允许用户在ReturnValueResolver
参数解析器解析参数的前后添加业务逻辑以及修改解析后的参数。接口定义
public interface ReturnValueResolverAdvice {
/**
* 使用此方法的返回值作为ReturnValueResolver.resolve()的参数调用
*/
Object beforeResolve(Object returnValue, AsyncRequest request, AsyncResponse response);
}
自定义ReturnValueResolverAdvice
与ArgumentResovler
相同, ReturnValueResolverAdvice
自定应时同样需要实现ReturnValueResolverAdviceAdapter
或ReturnValueResolverAdviceFactory
接口
方式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
接口的使用方式相同, 这里不过多赘述。
Note
ReturnValueResolverAdvice
与ReturnValueResolver
生命周期是相同的, 即应用初始化的时候便会决定每个参数的ReturnValueResolverAdvice
9 - 序列化
序列化支持
- jackson(默认)
- fastjson
- Gson
- ProtoBuf
- 自定义支持
Note
其中Json相关的序列化方式默认配置了日期格式为yyyy-MM-dd HH:mm:ss
序列化切换
默认使用Jackson序列化,因此Restlight也默认引入了Jackson依赖,如果想要切换成别的序列化方式,则需要引入对应的Maven依赖并进行简单的配置。
Example:以切换Gson为例
- 引入Gson依赖:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
- 注入Gson序列化器
@Bean
public HttpBodySerializer bodySerializer() {
return new GsonHttpBodySerializer();
}
完成切换。
Tip
如果不想继续引入默认的Jackson依赖可以使用<exclusions/>
标签排除, 同时一旦注入了自定义的序列化器原来默认的Jackson序列化器将不会再生效
序列化器
接口标准定义
HttpRequestSerializer
: 用于请求的反序列化。HttpResponseSerializer
: 用于响应的序列化。HttpBodySerializer
: 继承自HttpRequestSerializer
以及HttpResponseSerializer
, 可同时提供请求与响应的序列化功能。
内置的序列化器
FastJsonHttpBodySerializer
JacksonHttpBodySerializer
GsonHttpBodySerializer
ProtoBufHttpBodySerializer
Note
切换时引入Maven依赖并注入对应的序列化器即可定制序列化器
可以选择通过继承或者自定义实现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同样支持该标准,其作用逻辑如下图所示: 如图所示,配置文件中Jackson和Gson相关内容要想生效,需要满足如下条件:
- 用户没有手动定制HttpJsonBodySerializerAdapter
- 用户没有手动注入Jackson或者Gson序列化器(FastJsonHttpBodySerializer和GsonJsonHttpBodySerializer)
- 用户没有手动注入ObjectMapper或者Gson对象
- 配置文件有对Jackson和Gson进行配置
满足以上四个条件,配置文件中关于Jackson和Gson的配置,才会在对应的Jackson和Gson序列化器中生效。
响应序列化内容协商(Json与ProtoBuf序列化)
Note
前提: 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
Note
参数可选值:json
, pb
- 当参数
format
与应用自身的某些接口中的参数冲突时可以通过配置修改参数名称
restlight.server.serialize.request.negotiation-param=your-param-name
restlight.server.serialize.response.negotiation-param=your-param-name
Tip
此种方式实际上相当于在请求的Accept
字段中加入了application/json
, application/x-protobuf
值, 因此如果使用了自定义的序列化器,根据HttpResponseSerializer.supportsWrite(MediaType mediaType, Type type)
方法实现不同可能与实际行为不符。 具体请参考下文多个序列化器框架如何选择使用哪个序列化器实现
章节。
使用@RequestSerializer
指定请求序列化器
当配置了多个序列化器时, 可以使用@RequestSerializer
中指定使用某个固定的序列化方式
下面的Controller指定使用Jackson反序列化请求数据
@PostMapping("/foo")
public void bar(@RequestBody @RequestSerializer(JacksonHttpBodySerializer.class) User user) {
// ...
}
Tip
@RequestSerializer
可以在Prameter, Method以及Class上使用, 优先级依次递减。
使用@ResponseSerializer
指定响应序列化器
当配置了多个序列化器时, 可以使用@ResponseSerializer
中指定使用某个固定的序列化方式
下面的Controller指定使用Jackson序列化响应结果
@PostMapping("/foo")
@ResponseSerializer(JacksonHttpBodySerializer.class)
public Foo bar() {
return new Foo();
}
Tip
@ResponseSerializer
可以在Method以及Class上使用, 优先级依次递减。
使用@Serializer
指定请求 & 响应序列化器
当配置了多个序列化器时, 可以使用@Serializer
中指定使用某个固定的序列化方式, 相当于同时指定RequestSerializer
以及@ResponseSerializer
。
下面的Controller指定使用Jackson序列化
@PostMapping("/foo")
@Serializer(JacksonHttpBodySerializer.class)
public Foo bar(@RequestBody User user) {
return new Foo();
}
Tip
@ResponseSerializer
可以在Method以及Class上使用, 优先级依次递减。
多个序列化器框架如何选择使用哪个序列化器实现?
序列化器接口HttpRequestSerializer
, HttpResponseSerializer
中均定义了suppot(xxx)
方法, 用于在多个序列化器并存的情况下判断使用哪一个序列化器。以HttpRequestSerializer
为例
public interface HttpRequestSerializer extends BaseHttpSerializer, RxSerializer {
boolean supportsRead(MediaType mediaType, Type type);
}
HttpRequestSerializer
中定义了supportsRead(MediaType mediaType, Type type)
方法用于判断当前序列化器是否支持此次请求的序列化, 其中参数MediaType
为请求的ContentType
, Type
为当前反序列化的目标类型。
同样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;
}
};
}
多种序列化并存时的优先级
- 请求序列化
- 如果指定了
@RequestSerializer
或者@Serializer
中指定的序列化方式则使用该序列化方式 - 使用序列化协商中的参数指定的序列化器
- 根据
ContentType
查找序列化器 - 未找到则报错
- 响应序列化
- 如果指定了
@ResponseSerializer
或者@Serializer
中指定的序列化方式则使用该序列化方式 - 使用序列化协商中的参数指定的序列化器
- 根据
Accept
查找序列化器 - 根据
RequestMapping
中produces指定的序列化器(未找到进入6) - 未找到使用默认优先级最高的序列化器
ProtoBuf序列化支持
Restlight内置了ProtoBuf序列化器的实现,对应支持的MediaType为application/x-protobuf
(需要请求的Content-Type
为application/x-protobuf
同时使用ProtoBuf序列化器序列化后的响应结果的Content-Type
也为application/x-protobuf
)。
针对ProtoBuf序列化的特点,ProtoBuf序列化后还将增加Header, X-Protobuf-Schema
和X-Protobuf-Message
分别返回Message对象的getDescriptorForType().getFile().getName()
和getDescriptorForType().getFullName
的结果。
特殊类型返回值的序列化
不管Controller上是否加有@ResponseBody
注解, 在使用序列化器序列化之前都将遵守以下原则
- 值为String类型直接返回该字符串结果。
- 值为byte[]类型直接将结果写入响应。
- 值为ByteBuf类型时直接将结果写入响应
- 以上均不符合则使用序列化器进行序列化。
- 如果Controlelr上未配置
@ResponseBody
,值为基本类型或基本类型包装类将返回该类型的字符串结果(调用String.valueOf())。- 以上均不符合则抛异常。
10 - 请求参数聚合
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
}
Note
由于SpringMVC
的注解大多不支持在Field
上使用, 因此仅支持JAX-RS
注解以及自定义参数解析等场景。
11 - URL参数聚合
Content-Type
为application/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
}
Note
加了@QueryBean.Name(“name”)之后将使用提供的name作为参数名进行匹配, 原来的字段名字将不会使用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
Note
健康检查对应的请求路径不受全局Path影响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);
}
}
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%) 计算率: 理论上设置的越高实时性越强(缓存更新频率越高)但是并发损耗也会升高,因此建议设置的相对小一些以应对激增的非常用请求即可。
Note
除极端情况下通常来说此缓存效果都会比原生的遍历实现要高效(这里指的是请求在Controller上的分布完全均匀), 通常都能达到2-5倍的提升。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
其中,BIZ
和IO
为Scheduler的名称,type
为开始计时的方式,默认为QUEUED
,
表示从请求任务进入线程池排队时开始计时,TTFB
表示从接收到首字节时开始计时,time-millis
表示超时时间。
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()));
}
}
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...
};
}
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 | 客户端认证类型,不设置默认无 |
Tip
路径如果在classpath下请使用classpath:conf/foo.pem
的形式