1. 程式人生 > >jersey子資源api使用和原始碼分析

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. 總結

簡單概括下,就是:

  1. SubResourceLocatorRouter (子資源載入器方法) 上面必須要有Path 但是不能有 @HttpMethod,如上面的getMySubResource。
  2. 子資源類上面不能有 @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的資料結構

在這裡插入圖片描述