jersey子資源api使用和原始碼分析
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的資料結構