Jetty源码分析之ContextHandler

本文详细分析了Jetty的ContextHandler,它是ScopedHandler的子类,实现ServletContext并处理web应用上下文。在启动过程中,ContextHandler设置线程的ClassLoader和ServletContext,并处理监听器。在请求处理中,它负责URL路径处理,决定请求是否应由当前ContextHandler处理,同时防止对服务器受保护路径的访问。ContextHandler的初始化和请求处理是其两大核心功能,而更复杂的web应用初始化则在WebAppContext中完成。

ContextHandler是ScopedHandler的直接子类,继承关系比较简单就不贴类图了。
ContextHandler从名字的直观含义上来看是上下文Handler,在servlet规范中每个web应用都有一个上下文(context)的含义,其实就是对应到这里的ContextHandler了。当web容器收到用户请求之后,会根据请求里面的url先判断属于请求时发给哪个web应用的,但并不会将请求直接交给Servlet进行处理而是交给这个应用的ContextHandler。这是因为可能应用中配置的Servlet并不只有一个,还需要在一个地方根据请求的url再次转发给合适的Servlet;另外在将请求交给Servlet之前还需要进行一些判断和处理,这些工作都是在ContextHandler中完成的。另外ContextHandler还以内部类的形式实现了ServletContext。

下面先看下ContextHandler中的一些属性:

    //ServletContext,Servlet规范定义的接口,这里的内部类Context是其具体实现
    protected Context _scontext;
    private final AttributesMap _attributes;
    //对应web.xml中配置的初始化参数
    private final Map<String, String> _initParams;
    private ClassLoader _classLoader;
    //应用程序路径,可以在jetty.xml配置文件中配置,如果同时在jetty下部署了多个应用,则会默认以应用名称作为前缀
    private String _contextPath = "/";
    private String _contextPathEncoded = "/";

    //Context对应的名称
    private String _displayName;

    private Resource _baseResource;
    //文件类型
    private MimeTypes _mimeTypes;
    private Map<String, String> _localeEncodingMap;
    private String[] _welcomeFiles;
    //负责错误处理返回的Hanler,会返回一个默认的错误页面
    private ErrorHandler _errorHandler;
    //允许目标是这些主机的请求通过,如果不配置,则表示不对请求中的目标主机做限制
    private String[] _vhosts;

    private Logger _logger;
    //是否允许请求的url就是_contextPath的值,比如"/manager"(针对部署了多个应用的情况,contextPath需要根据加上每个应用名称做前缀)
    //这种情况下要想请求应用程序根路径应该是"/manager/"而不是"/manager".如果只部署了一个应用,contextPath可以设为"/",这种也就无所谓了.
    private boolean _allowNullPathInfo;
    private int _maxFormKeys = Integer.getInteger("org.eclipse.jetty.server.Request.maxFormKeys",-1).intValue();
    private int _maxFormContentSize = Integer.getInteger("org.eclipse.jetty.server.Request.maxFormContentSize",-1).intValue();
    //是否对传入的url进行路径压缩,路径压缩指的是将url中的多个“/"替换成一个"/"
    private boolean _compactPath = false;
    private boolean _usingSecurityManager = System.getSecurityManager()!=null;

    //一些监听器的配置,对应到web.xml中配置的各种listener
    private final List<EventListener> _eventListeners=new CopyOnWriteArrayList<>();
    private final List<EventListener> _programmaticListeners=new CopyOnWriteArrayList<>();
    private final List<ServletContextListener> _servletContextListeners=new CopyOnWriteArrayList<>();
    private final List<ServletContextListener> _destroySerletContextListeners=new ArrayList<>();
    private final List<ServletContextAttributeListener> _servletContextAttributeListeners=new CopyOnWriteArrayList<>();
    private final List<ServletRequestListener> _servletRequestListeners=new CopyOnWriteArrayList<>();
    private final List<ServletRequestAttributeListener> _servletRequestAttributeListeners=new CopyOnWriteArrayList<>();
    private final List<ContextScopeListener> _contextListeners = new CopyOnWriteArrayList<>();
    private final List<EventListener> _durableListeners = new CopyOnWriteArrayList<>();
    private Map<String, Object> _managedAttributes;

    //受保护的路径,比如WEB-INF/就是受保护的路径,应用程序是不能够访问的
    private String[] _protectedTargets;
    private final CopyOnWriteArrayList<AliasCheck> _aliasChecks = new CopyOnWriteArrayList<ContextHandler.AliasCheck>();

    //服务器状态,注意SHUTDOWN状态表示的是服务器将要关闭,不再接受新的请求但是已接收的请求会处理完成之后再关闭服务器,
    //增加这个状态可以实现服务器的平滑关闭
    public enum Availability { UNAVAILABLE,STARTING,AVAILABLE,SHUTDOWN,};
    private volatile Availability _availability = Availability.UNAVAILABLE;

注释中说明了大部分属性的含义。protected Context _scontext; 是一个ServletContext,这里的Context是ContextHandler中实现了ServletContext接口的一个内部类。并且通过一个ThreadLocal静态变量将_scontext声明成了可全局共享的。

//ThreadLocal变量,用来保证在当前线程中可以随时取到ServletContext对象
    private static final ThreadLocal<Context> __context = new ThreadLocal<Context>();

ContextHandler实现了CycleLife和Handler接口,所以下面再看下启动过程和请求处理过程。
先是启动过程,ContextHandler重写了doStart()方法。

protected void doStart() throws Exception
    {
        //设置启动状态
        _availability = Availability.STARTING;

        if (_contextPath == null)
            throw new IllegalStateException("Null contextPath");

        if (_logger == null)
        {
            _logger = Log.getLogger(ContextHandler.class.getName() + getLogNameSuffix());
        }

        ClassLoader old_classloader = null;
        Thread current_thread = null;
        Context old_context = null;

        //设置全局属性
        _attributes.setAttribute("org.eclipse.jetty.server.Executor",getServer().getThreadPool());

        if (_mimeTypes == null)
            _mimeTypes = new MimeTypes();

        try
        {
            // Set the classloader, context and enter scope
            //这里的ClassLoader一般是子类定义的
            if (_classLoader != null)
            {
                current_thread = Thread.currentThread();
                old_classloader = current_thread.getContextClassLoader();
                current_thread.setContextClassLoader(_classLoader);
            }
            old_context = __context.get();
            __context.set(_scontext);
            enterScope(null, getState());

            // defers the calling of super.doStart()
            startContext();

            _availability = Availability.AVAILABLE;
            LOG.info("Started {}", this);
        }
        finally
        {
            if (_availability==Availability.STARTING)
                _availability=Availability.UNAVAILABLE;
            exitScope(null);
            __context.set(old_context);
            // reset the classloader
            if (_classLoader != null && current_thread!=null)
                current_thread.setContextClassLoader(old_classloader);
        }
    }

这里面先是给当前线程重新设置了ClassLoader和ServletContext,并且将旧的值放置到了局部变量中存储。另外在启动中加了一步startContext(),下面是其源码:

 protected void startContext() throws Exception
    {
        //增加一个Attributes事件监听器
        String managedAttributes = _initParams.get(MANAGED_ATTRIBUTES);
        if (managedAttributes != null)
            addEventListener(new ManagedAttributeListener(this,StringUtil.csvSplit(managedAttributes)));

        super.doStart();

        // Call context listeners
        //将容器初始化成功事件通知给监听器
        _destroySerletContextListeners.clear();
        if (!_servletContextListeners.isEmpty())
        {
            ServletContextEvent event = new ServletContextEvent(_scontext);
            for (ServletContextListener listener:_servletContextListeners)
            {
                callContextInitialized(listener, event);
                _destroySerletContextListeners.add(listener);
            }
        }
    }

可以看到逻辑并不复杂,只是增加了一些监听器的处理流程。

再来看下请求处理流程。ContextHandler继承自ScopedHandler,请求处理主要是在doScope()和doHandle()两个方法中进行的,ContextHandler对这两个方法都进行了重写。先看下doScope()方法:

public void doScope(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
    {
        if (LOG.isDebugEnabled())
            LOG.debug("scope {}|{}|{} @ {}",baseRequest.getContextPath(),baseRequest.getServletPath(),baseRequest.getPathInfo(),this);

        Context old_context = null;
        String old_context_path = null;
        String old_servlet_path = null;
        String old_path_info = null;
        ClassLoader old_classloader = null;
        Thread current_thread = null;
        String pathInfo = target;

        //请求类型
        DispatcherType dispatch = baseRequest.getDispatcherType();

        //请求对应的ServeltContext,对于新连接建立请求来说一般都为null
        old_context = baseRequest.getContext();

        // Are we already in this context?
        //里面主要进行请求合法性检查,包括请求路径和请求中的主机名的检查,并重新设置当前线程的ClassLoader
        if (old_context != _scontext)
        {
            // check the target.
            if (DispatcherType.REQUEST.equals(dispatch) ||
                DispatcherType.ASYNC.equals(dispatch) ||
                DispatcherType.ERROR.equals(dispatch) && baseRequest.getHttpChannelState().isAsync())
            {
                if (_compactPath)//对请求进行压缩处理,比如"///"压缩成“/”
                    target = URIUtil.compactPath(target);
                if (!checkContext(target,baseRequest,response))
                    return;

                //先是对请求url的处理
                if (target.length() > _contextPath.length())
                {
                    //如果contextPath长度大于1,则意味着前缀一般是应用的名称,在请求的url中要把这部分去掉
                    if (_contextPath.length() > 1)
                        target = target.substring(_contextPath.length());
                    pathInfo = target;
                }
                else if (_contextPath.length() == 1)
                {
                    target = URIUtil.SLASH;
                    pathInfo = URIUtil.SLASH;
                }
                else
                {
                    target = URIUtil.SLASH;
                    pathInfo = null;
                }
            }

            // Set the classloader
            if (_classLoader != null)
            {
                current_thread = Thread.currentThread();
                old_classloader = current_thread.getContextClassLoader();
                current_thread.setContextClassLoader(_classLoader);
            }
        }

        try
        {
            old_context_path = baseRequest.getContextPath();
            old_servlet_path = baseRequest.getServletPath();
            old_path_info = baseRequest.getPathInfo();

            // Update the paths
            //更新请求的ServletContext
            baseRequest.setContext(_scontext);
            __context.set(_scontext);
            if (!DispatcherType.INCLUDE.equals(dispatch) && target.startsWith("/"))
            {
                if (_contextPath.length() == 1)
                    baseRequest.setContextPath("");
                else
                    baseRequest.setContextPath(_contextPathEncoded);
                baseRequest.setServletPath(null);
                baseRequest.setPathInfo(pathInfo);
            }

            if (old_context != _scontext)
                enterScope(baseRequest,dispatch);

            if (LOG.isDebugEnabled())
                LOG.debug("context={}|{}|{} @ {}",baseRequest.getContextPath(),baseRequest.getServletPath(), baseRequest.getPathInfo(),this);

            //继续scope链的调用
            nextScope(target,baseRequest,request,response);
        }
        finally
        {
            if (old_context != _scontext)
            {
                exitScope(baseRequest);

                // reset the classloader
                if (_classLoader != null && current_thread!=null)
                {
                    current_thread.setContextClassLoader(old_classloader);
                }

                // reset the context and servlet path.
                //重设ServletContext和servletPath(处理从其它servlet转发过来的请求???)
                baseRequest.setContext(old_context);
                __context.set(old_context);
                baseRequest.setContextPath(old_context_path);
                baseRequest.setServletPath(old_servlet_path);
                baseRequest.setPathInfo(old_path_info);
            }
        }
    }

可以看到主要是对请求中的url进行一些处理,包括路径压缩和前缀的处理,并且通过请求的url判断(包括URL与ContextPath路径比较,请求中目标主机与该ContextHandler中的虚拟主机列表匹配)该请求是否应该放到当前的ContextHandler进行处理。另外就是重设了请求中ServletContext、servletPath等信息,当请求处理完成返回的时候又会将旧的值填充进去。

然后再看下doHandle()中的流程:

public void doHandle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
    {
        final DispatcherType dispatch = baseRequest.getDispatcherType();
        final boolean new_context = baseRequest.takeNewContext(); //第一次获取请求中的ServletContext 则为true
        try
        {
            if (new_context) //请求初始化工作,主要是为请求添加ServletRequestAttributeListener监听器,并将请求开始处理这个事件通知给监听器
                requestInitialized(baseRequest,request);

            switch(dispatch)
            {
                case REQUEST: //重复请求
                    if (isProtectedTarget(target))//如果请求的是受保护的资源,则直接返回错误
                    {
                        response.sendError(HttpServletResponse.SC_NOT_FOUND);
                        baseRequest.setHandled(true);
                        return;
                    }
                    break;

                case ERROR://错误请求,直接返回错误页面
                    // If this is already a dispatch to an error page, proceed normally
                    if (Boolean.TRUE.equals(baseRequest.getAttribute(Dispatcher.__ERROR_DISPATCH)))
                        break;

                    // We can just call doError here.  If there is no error page, then one will
                    // be generated. If there is an error page, then a RequestDispatcher will be
                    // used to route the request through appropriate filters etc.
                    doError(target,baseRequest,request,response);
                    return;
                default:
                    break;
            }

            //继续doHandle()或doScope()处理
            nextHandle(target,baseRequest,request,response);
        }
        finally
        {
            if (new_context)
                requestDestroyed(baseRequest,request);
        }
    }

可以看到逻辑还是比较简单的,主要注意这里对请求的路径有个限制,如果请求的是服务器上受保护的路径(默认是WEB-INF和META-INF),那么会直接返回404错误。

启动和请求处理流程分析完成之后,ContextHandler就大致分析完了。总的来说,主要分为两大部分:第一部分就是启动时候的初始化,ContextHandler初始化主要是重设了下servletContxt和当前线程的ClassLoader和对监听器的处理包括将容器初始化成功的事件通知给注册的ServletContextListener监听器。第二部分是请求处理,先是通过请求的url和请求中的目标主机名称判断该请求是否需要当前的ContextHandler进行处理,如果需要的话会将请求转发给ServletHandler等具体处理请求的Handler进行处理,另外还会给请求增加一些Listener。
ContextHandler对于请求的处理可以说是基本完备的,但是对于启动过程的处理过于简单了,因为在WebApplication启动的时候,面对的是一个war包(或者是war包解压之后的一堆文件),ContextHandler的启动的时候是需要对这个war包进行处理的,包括解压war包、处理里面的包含的依赖jar包文件、处理里面应用自定义的web.xml文件等。其实这些处理都是在ContextHandler的子类WebAppContext中进行的,下一篇文章就可以看到这些重要的初始化流程。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值