1、前言
1.1 描述
查看eureka server源码时候,用到了jersey实现api功能,其中包含了子资源路由api功能。
这里主要分析下子资源的实现逻辑。
1.2 jersey简介
jersey Jersey RESTful 框架是开源的RESTful框架, 实现了JAX-RS (JSR 311 & JSR 339) 规范。
它扩展了JAX-RS 参考实现, 提供了更多的特性和工具, 可以进一步地简化 RESTful service 和 client 开发。尽管相对年轻,它已经是一个产品级的 RESTful service 和 client 框架。与Struts类似,它同样可以和hibernate,spring框架整合。(截取自百度百科)
使用可以参考https://jersey.github.io/documentation/latest/getting-started.html,
2、子资源举例
eureka-core-1.4.12.jar中的
com.netflix.eureka.resources.ApplicationsResource#getApplicationResource就是子资源入口方法,返回子资源对象 com.netflix.eureka.resources.ApplicationResource。
3 子资源api实现demo
3.1 父资源中SubResourceLocators(子资源加载器方法)声明
父资源中,需要返回子资源的方法有特殊限制,方法上必须要有Path 但是不能有@HttpMethod,如下面的getMySubResource。
package com.example;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
/**
* Root resource (exposed at "myresource" path)
*/
@Path("myresource")
public class MyResource {
/**
* Method handling HTTP GET requests. The returned object will be sent
* to the client as "text/plain" media type.
*
* @return String that will be returned as a text/plain response.
*/
@GET
@Produces(MediaType.TEXT_PLAIN)
public String getIt() {
return "Got it!";
}
// 子资源必须要有Path 但是没有 @HttpMethod
@Path("sub")
public MySubResource getMySubResource() {
return new MySubResource();
}
// 子资源必须要有Path 但是没有@HttpMethod,这个只是一个普通呃请求
@GET
@Path("sub2")
@Produces(MediaType.APPLICATION_JSON)
public MySubResource getResInfo() {
return new MySubResource();
}
// 子资源必须要有Path 但是这个对象里面没有匹配到请求,404 @HttpMethod 会被忽略掉
@Path("sub3")
public String getRes() {
return "will.be.404";
}
}
3.2 子资源声明
子资源类上面没有 @Path
package com.example;
import javax.ws.rs.GET;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
/**
* Root resource (exposed at "myresource" path)
*/
// 子资源没有 @Path
public class MySubResource {
private String info = "this is sub resource.";
/**
* Method handling HTTP GET requests. The returned object will be sent
* to the client as "text/plain" media type.
*
* @return String that will be returned as a text/plain response.
*/
@GET
@Produces(MediaType.TEXT_PLAIN)
public String getIt() {
return "Got it sub!";
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
@Override public String toString() {
return "MySubResource{" + "info='" + info + '\'' + '}';
}
}
3.3 验证
-
请求http://localhost:8080/webapi/myresource,返回Got it!,最普通的请求。
-
请求http://localhost:8080/webapi/myresource/sub,返回Got it sub!说明走到子资源的getIt方法,验证了子资源api的实现。
-
请求http://localhost:8080/webapi/myresource/sub2,返回
{
info: "this is sub resource.",
it: "Got it sub!"
}
说明这个接口是一个普通的接口,因为方法上面有@GET。
- 请求http://localhost:8080/webapi/myresource/sub3
4. 总结
简单概括下,就是:
- SubResourceLocatorRouter (子资源加载器方法) 上面必须要有Path 但是不能有 @HttpMethod,如上面的getMySubResource。
- 子资源类上面不能有 @Path。
5. 源码分析
这里拿demo简单分析下子资源api咋实现的,eureka的版本不同,但是思路类似。
从两个方面入手,一个是资源配置的解析,一个是请求逻辑的处理。
5.1 资源配置解析
主要解析出来哪些method属于子资源加载方法SubResourceLocatorRouter,放入最终路由列表中。
入口,首先看下web.xml,spring cloud通过@EnableEurekaServer引入入口逻辑。
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<servlet>
<servlet-name>Jersey Web Application</servlet-name>
<servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>jersey.config.server.provider.packages</param-name>
<param-value>com.example,org.codehaus.jackson.jaxrs</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Jersey Web Application</servlet-name>
<url-pattern>/webapi/*</url-pattern>
</servlet-mapping>
</web-app>
org.glassfish.jersey.servlet.ServletContainer
@Override
public void init() throws ServletException {
init(new WebServletConfig(this));
}
主要就是init(new WebFilterConfig(filterConfig));逻辑。逻辑最终会走到
org.glassfish.jersey.server.model.IntrospectionModeller#doCreateResourceBuilder
private Resource.Builder doCreateResourceBuilder() {
if (!disableValidation) {
checkForNonPublicMethodIssues();
}
final Class<?> annotatedResourceClass = ModelHelper.getAnnotatedResourceClass(handlerClass);
// 这个是获取是否存在Path注解
final Path rPathAnnotation = annotatedResourceClass.getAnnotation(Path.class);
final boolean keepEncodedParams =
(null != annotatedResourceClass.getAnnotation(Encoded.class));
final List<MediaType> defaultConsumedTypes =
extractMediaTypes(annotatedResourceClass.getAnnotation(Consumes.class));
final List<MediaType> defaultProducedTypes =
extractMediaTypes(annotatedResourceClass.getAnnotation(Produces.class));
final Collection<Class<? extends Annotation>> defaultNameBindings =
ReflectionHelper.getAnnotationTypes(annotatedResourceClass, NameBinding.class);
final MethodList methodList = new MethodList(handlerClass);
final List<Parameter> resourceClassParameters = new LinkedList<>();
checkResourceClassSetters(methodList, keepEncodedParams, resourceClassParameters);
checkResourceClassFields(keepEncodedParams, InvocableValidator.isSingleton(handlerClass), resourceClassParameters);
Resource.Builder resourceBuilder;
// 有Path注解,会放入路径
if (null != rPathAnnotation) {
resourceBuilder = Resource.builder(rPathAnnotation.value());
} else {
resourceBuilder = Resource.builder();
}
boolean extended = false;
if (handlerClass.isAnnotationPresent(ExtendedResource.class)) {
resourceBuilder.extended(true);
extended = true;
}
resourceBuilder.name(handlerClass.getName());
addResourceMethods(resourceBuilder, methodList, resourceClassParameters, keepEncodedParams,
defaultConsumedTypes, defaultProducedTypes, defaultNameBindings, extended);
addSubResourceMethods(resourceBuilder, methodList, resourceClassParameters, keepEncodedParams,
defaultConsumedTypes, defaultProducedTypes, defaultNameBindings, extended);
//将方法加入到子资源列表中
addSubResourceLocators(resourceBuilder, methodList, resourceClassParameters, keepEncodedParams, extended);
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.finest(LocalizationMessages.NEW_AR_CREATED_BY_INTROSPECTION_MODELER(
resourceBuilder.toString()));
}
return resourceBuilder;
}
进入org.glassfish.jersey.server.model.IntrospectionModeller#addSubResourceLocators,会针对子资源加载器构建一个Resource.Builder
private void addSubResourceLocators(
final Resource.Builder resourceBuilder,
final MethodList methodList,
final List<Parameter> resourceClassParameters, // parameters derived from fields and setters on the resource class
final boolean encodedParameters,
final boolean extended) {
// 只有方法上没有HttpMethod注解,但是有Path注解,才会进入该逻辑,该逻辑中会创建一个针对子资源加载器的builder。
for (AnnotatedMethod am : methodList.withoutMetaAnnotation(HttpMethod.class).withAnnotation(Path.class)) {
final String path = am.getAnnotation(Path.class).value();
Resource.Builder builder = resourceBuilder;
if (path != null && !path.isEmpty() && !"/".equals(path)) {
builder = resourceBuilder.addChildResource(path);
}
builder.addMethod()
.encodedParameters(encodedParameters || am.isAnnotationPresent(Encoded.class))
.handledBy(handlerClass, am.getMethod())
.handlingMethod(am.getDeclaredMethod())
.handlerParameters(resourceClassParameters)
.extended(extended || am.isAnnotationPresent(ExtendedResource.class));
}
}
上面的builder后续会build出来一个ResourceMethod,在build过程中 会判断type 是否是子资源加载器,具体如下:
/**
* Build the resource method model and register it with the parent
* {@link Resource.Builder Resource.Builder}.
*
* @return new resource method model.
*/
public ResourceMethod build() {
// 构建method数据,区别是否子资源加载器方法。
final Data methodData = new Data(
httpMethod,
consumedTypes,
producedTypes,
managedAsync,
suspended,
sse,
suspendTimeout,
suspendTimeoutUnit,
createInvocable(),
nameBindings,
parent.isExtended() || extended);
parent.onBuildMethod(this, methodData);
return new ResourceMethod(null, methodData);
}
org.glassfish.jersey.server.model.ResourceMethod.Data#Data 判断是否是子资源加载器
private Data(final String httpMethod,
final Collection<MediaType> consumedTypes,
final Collection<MediaType> producedTypes,
boolean managedAsync, final boolean suspended, boolean sse,
final long suspendTimeout,
final TimeUnit suspendTimeoutUnit,
final Invocable invocable,
final Collection<Class<? extends Annotation>> nameBindings,
final boolean extended) {
this.managedAsync = managedAsync;
// 判断资源加载类型
this.type = JaxrsType.classify(httpMethod);
this.httpMethod = (httpMethod == null) ? httpMethod : httpMethod.toUpperCase();
this.consumedTypes = Collections.unmodifiableList(new ArrayList<>(consumedTypes));
this.producedTypes = Collections.unmodifiableList(new ArrayList<>(producedTypes));
this.invocable = invocable;
this.suspended = suspended;
this.sse = sse;
this.suspendTimeout = suspendTimeout;
this.suspendTimeoutUnit = suspendTimeoutUnit;
this.nameBindings = Collections.unmodifiableCollection(new ArrayList<>(nameBindings));
this.extended = extended;
}
private static JaxrsType classify(String httpMethod) {
// 看看有没有httpMethod注解
if (httpMethod != null && !httpMethod.isEmpty()) {
return RESOURCE_METHOD;
} else {
return SUB_RESOURCE_LOCATOR;
}
}
最终会通过这些数据生成 org.glassfish.jersey.server.internal.routing.MatchResultInitializerRouter#rootRouter,这是一个最终路由列表,其中包含jersey的所有路由信息。
从上面很容易就可以看出来,没有httpMethod的就是子资源加载器方法,再结合前面放入子资源时候的@path判断,就得出来我们的条件,子资源加载器方法(SubResourceLocatorRouter)上面必须要有Path 但是不能有 @HttpMethod,如上面的getMySubResource。
5.2 资源请求匹配
主要分析下一个资源请求过来,如何通过子资源加载器处理逻辑。
核心入口在org.glassfish.jersey.server.internal.routing.RoutingStage#apply。
其中包含Routing stage逻辑,stage是一种责任链模式,类似于filter,Routing stage逻辑是针对路由的链式处理。
/**
* {@inheritDoc}
* <p/>
* Routing stage navigates through the nested {@link Router routing hierarchy}
* using a depth-first transformation strategy until a request-to-response
* inflector is {@link org.glassfish.jersey.process.internal.Inflecting found on
* a leaf stage node}, in which case the request routing is terminated and an
* {@link org.glassfish.jersey.process.Inflector inflector} (if found) is pushed
* to the {@link RoutingContext routing context}.
*/
@Override
public Continuation<RequestProcessingContext> apply(final RequestProcessingContext context) {
final ContainerRequest request = context.request();
context.triggerEvent(RequestEvent.Type.MATCHING_START);
final TracingLogger tracingLogger = TracingLogger.getInstance(request);
final long timestamp = tracingLogger.timestamp(ServerTraceEvent.MATCH_SUMMARY);
try {
// 根据请求url匹配路由表正则,不断的从顶级路由表往下找,分级匹配url。
final RoutingResult result = _apply(context, routingRoot);
Stage<RequestProcessingContext> nextStage = null;
if (result.endpoint != null) {
context.routingContext().setEndpoint(result.endpoint);
nextStage = getDefaultNext();
}
return Continuation.of(result.context, nextStage);
} finally {
tracingLogger.logDuration(ServerTraceEvent.MATCH_SUMMARY, timestamp);
}
}
对于子资源类型的请求,这里会逐级查找。首先找到前缀匹配的的顶级路由,然后根据顶级路由进行查找。下面便是一个资源匹配的逻辑。
@Override
public Router.Continuation apply(final RequestProcessingContext context) {
final RoutingContext rc = context.routingContext();
// Peek at matching information to obtain path to match
String path = rc.getFinalMatchingGroup();
final TracingLogger tracingLogger = TracingLogger.getInstance(context.request());
tracingLogger.log(ServerTraceEvent.MATCH_PATH_FIND, path);
Router.Continuation result = null;
final Iterator<Route> iterator = acceptedRoutes.iterator();
while (iterator.hasNext()) {
final Route acceptedRoute = iterator.next();
final PathPattern routePattern = acceptedRoute.routingPattern();
final MatchResult m = routePattern.match(path);
if (m != null) {
// Push match result information and rest of path to match
rc.pushMatchResult(m);
// 返回的acceptedRoute.next() 包含当期资源的所有匹配路径,其中就包括子资源路径
result = Router.Continuation.of(context, acceptedRoute.next());
//tracing
tracingLogger.log(ServerTraceEvent.MATCH_PATH_SELECTED, routePattern.getRegex());
break;
} else {
tracingLogger.log(ServerTraceEvent.MATCH_PATH_NOT_MATCHED, routePattern.getRegex());
}
}
if (tracingLogger.isLogEnabled(ServerTraceEvent.MATCH_PATH_SKIPPED)) {
while (iterator.hasNext()) {
tracingLogger.log(ServerTraceEvent.MATCH_PATH_SKIPPED, iterator.next().routingPattern().getRegex());
}
}
if (result == null) {
// No match
return Router.Continuation.of(context);
}
return result;
}
在前面的资源配置加载中,我们已经说了,最终会根据SUB_RESOURCE_LOCATOR 类型生成子资源加载器路由。这个地方递归匹配请求,最终就会找到这个SubResourceLocatorRouter。
在org.glassfish.jersey.server.internal.routing.SubResourceLocatorRouter#getResource中便是SubResourceLocatorRoute获取子资源对象逻辑,是一个反射逻辑。
private Object getResource(final RequestProcessingContext context) {
final Object resource = context.routingContext().peekMatchedResource();
final Method handlingMethod = locatorModel.getInvocable().getHandlingMethod();
final Object[] parameterValues = ParameterValueHelper.getParameterValues(valueProviders, context.request());
context.triggerEvent(RequestEvent.Type.LOCATOR_MATCHED);
final PrivilegedAction invokeMethodAction = () -> {
try {
// 根据method 反射获取子资源对象
return handlingMethod.invoke(resource, parameterValues);
} catch (IllegalAccessException | IllegalArgumentException | UndeclaredThrowableException ex) {
throw new ProcessingException(LocalizationMessages.ERROR_RESOURCE_JAVA_METHOD_INVOCATION(), ex);
} catch (final InvocationTargetException ex) {
final Throwable cause = ex.getCause();
if (cause instanceof WebApplicationException) {
throw (WebApplicationException) cause;
}
// handle all exceptions as potentially mappable (incl. ProcessingException)
throw new MappableException(cause);
} catch (final Throwable t) {
throw new ProcessingException(t);
}
};
final SecurityContext securityContext = context.request().getSecurityContext();
return (securityContext instanceof SubjectSecurityContext)
? ((SubjectSecurityContext) securityContext).doAsSubject(invokeMethodAction) : invokeMethodAction.run();
}
综上,简单来说,一个请求过来,就是根据路由表匹配,首先匹配顶级资源的@path等信息;
然后匹配各个资源里面的子资源,这些子资源包含普通的二级path配置和特殊的子资源加载器方法配置。
最终根据匹配上的路由处理逻辑,如果是普通的资源api,就反射处理;如果是子资源加载器路由,会首先反射获取到子资源对象,然后再反射处理。
附上一个router的数据结构