jersey子资源api使用和源码分析

本文深入解析Jersey框架中子资源API的实现原理,包括资源配置解析与请求处理流程,阐述了子资源加载器方法的定义规则及其实现机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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的数据结构

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

这是一个懒人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值