JavaSE 手写 Web 服务器(二)

726 阅读6分钟
原文链接: www.extlight.com

一、背景

在上一篇文章 《JavaSE 手写 Web 服务器(一)》 中介绍了编写 web 服务器的初始模型,封装请求与响应和多线程处理的内容。但是,还是遗留一个问题:如何根据不同的请求 url 去触发不同的业务逻辑。

这个问题将在本篇解决。

二、涉及知识

XML:将配置信息写到 XML 文件,解决硬编码问题。

反射:读取 XML 文件配置并实例化对象。

三、封装控制器

目前手写的 web 容器只能处理一种业务请求,无论发送什么 url 的请求都是只返回一个内容相似的页面。

为了能很好的扩展 web 容器处理不同请求的功能,我们需要将 request 和 response 封装到控制器,让各个业务的控制器处理对应请求和响应。

# 3.1 抽离控制器

创建一个父类控制器:

public class Servlet {

    

    public void service(Request request, Response response) {

        doGet(request,response);

        doPost(request,response);

    }



    protected void doGet(Request request, Response response) {

        

    }

    

    protected void doPost(Request request, Response response) {

        

    }

}

父类中使用了模板方法的设计模式,将业务处理的行为交给子类去处理。

当我们需要一个控制器去处理登陆请求时,我们创建一个 LoginServlet 类去继承 Servlet 并重写 doGet 或 doPost 方法:

public class LoginServlet extends Servlet {



    @Override

    protected void doPost(Request request, Response response) {

        response.println("<!DOCTYPE html>")

        .println("<html lang=\"zh\">")

        .println("    <head>      ")

        .println("        <meta charset=\"UTF-8\">")

        .println("        <title>测试</title>")

        .println("    </head>     ")

        .println("    <body>      ")

        .println("        <h3>Hello " + request.getParameter("username") + "</h3>")// 获取登陆名

        .println("    </body>     ")

        .println("</html>");

    }



}

如果需要处理注册请求,创建一个 RegisterServlet 类继承 Servlet 并重写 doGet 或 doPost 方法即可。

# 3.2 创建 web 容器上下文

为了能更好的管理多个控制器实例以及请求 url 与控制器的对应关系,我们需要一个类对其封装和管理。

/**

 *  web 容器上下文

 * @author Light

 *

 */

public class ServletContext {



    // 存储处理不同请求的 servlet

    // servletName -> servlet 子类

    private Map<String,Servlet> servletMap;

    

    // 存储请求 url 与 servlet 的对应关系

    // 请求 url -> servletName

    private Map<String,String> mappingMap;

    

    public ServletContext() {

        this.servletMap = new HashMap<String, Servlet>();

        this.mappingMap = new HashMap<String, String>();

    }



    public Map<String, Servlet> getServletMap() {

        return servletMap;

    }



    public void setServletMap(Map<String, Servlet> servletMap) {

        this.servletMap = servletMap;

    }



    public Map<String, String> getMappingMap() {

        return mappingMap;

    }



    public void setMappingMap(Map<String, String> mappingMap) {

        this.mappingMap = mappingMap;

    }

    

}

# 3.3 创建配置类

虽然有了 web 容器上下文,但是 web 容器上下文并不是一开始就存放配置信息的。配置信息在 web 容器启动时被注册或写入到上下文中,因此需要一个管理配置的类负责该操作:

/**

 * 配置文件类

 * @author Light

 *

 */

public class WebApp {



    private static ServletContext context;

    

    static {

        context = new ServletContext();

        

        Map<String,Servlet> servletMap = context.getServletMap();

        Map<String,String> mappingMap = context.getMappingMap();

        

        // 注册 登陆控制器

        servletMap.put("login", new LoginServlet());

        mappingMap.put("/login", "login");

        

        // 如有更多请求需要处理,在此配置对应的控制器即可

    }

    

    /**

     *  通过请求 url 获取对应的 Servlet 对象

     * @param url

     * @return

     */

    public static Servlet getServlet(String url) {

        if (url == null || "".equals(url.trim())) {

            return null;

        }

        

        String servletName = context.getMappingMap().get(url);

        Servlet servlet = context.getServletMap().get(servletName);

        

        return servlet;

    }

}

改造 Dispatcher 的 run 方法,从 WebApp 类中获取控制器实例:

@Override

public void run() {

    // 获取控制器

    Servlet servlet = WebApp.getServlet(this.request.getUrl());

    // 处理请求

    servlet.service(request, response);

    try {

        this.response.pushToClient(code);

        this.socket.close();

    } catch (IOException e) {

        e.printStackTrace();

    }

    

}

四、解决硬编码问题

在 WebApp 类中,我们配置了 LoginServlet 类以及相关信息,这种写法属于硬编码,且这个两个类发生了耦合。

为了解决上述问题,我们可以将 LoginServlet 类的配置写到一个 xml 文件中,WebApp 类负责读取和解析 xml 文件中的信息并将信息写入到 web 容器上下文中,在 Dispatcher 类中通过反射实例化控制器对象处理请求。

创建 web.xml 配置文件:

<?xml version="1.0" encoding="UTF-8"?>

<web-app>

    <servlet>

        <servlet-name>login</servlet-name>

        <servlet-class>com.light.controller.LoginServlet</servlet-class>

    </servlet>

    <servlet-mapping>

        <servlet-name>login</servlet-name>

        <url-pattern>/login</url-pattern>

    </servlet-mapping>

</web-app>

创建两个类用于封装 web.xml 配置文件中的数据,即 <servlet> 和 <servlet-mapping> 相关标签的内容:

/**

 * 封装 web.xml 中 <servlet> 配置信息

 * @author Light

 *

 */

public class ServletXml {



    private String servletName;

    

    private String servletClazz;



    public String getServletName() {

        return servletName;

    }



    public void setServletName(String servletName) {

        this.servletName = servletName;

    }



    public String getServletClazz() {

        return servletClazz;

    }



    public void setServletClazz(String servletClazz) {

        this.servletClazz = servletClazz;

    }



}





/**

 * 封装 web.xml 中 <servlet-mapping> 配置信息

 * @author Light

 *

 */

public class ServletMappingXml {



    private String servletName;

    

    private List<String> urlPattern = new ArrayList<>();



    public String getServletName() {

        return servletName;

    }



    public void setServletName(String servletName) {

        this.servletName = servletName;

    }



    public List<String> getUrlPattern() {

        return urlPattern;

    }



    public void setUrlPattern(List<String> urlPattern) {

        this.urlPattern = urlPattern;

    }

    

}

创建 xml 文件解析器,用于解析 web.xml 配置文件:

/**

 * xml文件解析器

 * @author Light

 *

 */

public class WebHandler extends DefaultHandler{

    

    private List<ServletXml> servletXmlList;

    private List<ServletMappingXml> mappingXmlList;

    

    private ServletXml servletXml;

    private ServletMappingXml servletMappingXml;

    

    private String beginTag;

    private boolean isMapping;

    

    



    @Override

    public void startDocument() throws SAXException {

        servletXmlList = new ArrayList<>();

        mappingXmlList = new ArrayList<>();

    }



    @Override

    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {

        if (qName != null) {

            beginTag = qName;

            

            if ("servlet".equals(qName)) {

                isMapping = false;

                servletXml = new ServletXml();

            } else if ("servlet-mapping".equals(qName)){

                isMapping = true;

                servletMappingXml = new ServletMappingXml();

            }

        }

    }

    

    @Override

    public void characters(char[] ch, int start, int length) throws SAXException {

        if (this.beginTag != null) {

            String str = new String(ch,start,length);

            

            if(isMapping) {

                if("servlet-name".equals(beginTag)) {

                    servletMappingXml.setServletName(str);

                } else if ("url-pattern".equals(beginTag)) {

                    servletMappingXml.getUrlPattern().add(str);

                }

                

            } else {

                if("servlet-name".equals(beginTag)) {

                    servletXml.setServletName(str);

                } else if ("servlet-class".equals(beginTag)) {

                    servletXml.setServletClazz(str);

                }

                

            }

        }

    }



    @Override

    public void endElement(String uri, String localName, String qName) throws SAXException {

        if (qName != null) {

            

            if ("servlet".equals(qName)) {

                this.servletXmlList.add(this.servletXml);

            } else if ("servlet-mapping".equals(qName)){

                this.mappingXmlList.add(this.servletMappingXml);

            }

        }

        this.beginTag = null;

    }

    

    public List<ServletXml> getServletXmlList() {

        return servletXmlList;

    }



    public List<ServletMappingXml> getMappingXmlList() {

        return mappingXmlList;

    }



}

改造 ServletContext 类:

// 存储处理不同请求的 servlet

// servletName -> servlet 子类的全名

private Map<String,String> servletMap;

即 将 private Map<String,Servlet> servletMap 改成 private Map<String,String> servletMap 。

注意,get 和 set 方法都需要修改。

改造 WebApp 类中注册控制器相关代码:

/**

 * 配置文件类

 * @author Light

 *

 */

public class WebApp {



    private static ServletContext context;

    

    static {

        context = new ServletContext();

        Map<String,String> servletMap = context.getServletMap();

        Map<String,String> mappingMap = context.getMappingMap();

        

        try {

            SAXParserFactory factory = SAXParserFactory.newInstance();

            SAXParser sax = factory.newSAXParser();

            WebHandler handler = new WebHandler();

            

            // 读取和解析 xml 文件

            sax.parse(Thread

                    .currentThread()

                    .getContextClassLoader()

                    .getResourceAsStream("com/light/server/web.xml"), 

                    handler);

            

            // 注册 servlet

            List<ServletXml> servletXmlList = handler.getServletXmlList();

            for (ServletXml servletXml : servletXmlList) {

                servletMap.put(servletXml.getServletName(), servletXml.getServletClazz());

            }

            

            // 注册 mapping

            List<ServletMappingXml> mappingXmlList = handler.getMappingXmlList();

            for (ServletMappingXml mapping : mappingXmlList) {

                List<String> urls = mapping.getUrlPattern();

                for (String url : urls) {

                    mappingMap.put(url, mapping.getServletName());

                }

            }

            

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

    

    /**

     *  通过请求 url 获取对应的 Servlet 对象

     * @param url

     * @return

     */

    public static String getServletClazz(String url) {

        if (url == null || "".equals(url.trim())) {

            return null;

        }

        

        String servletName = context.getMappingMap().get(url);

        String servletClazz = context.getServletMap().get(servletName);

        

        return servletClazz;

    }

}

改造 Dispatcher 类的 run 方法,通过反射实例化对象:

@Override

public void run() {

    try {

        // 获取控制器包名

        String servletClazz = WebApp.getServletClazz(this.request.getUrl());

        // 通过反射实例化控制器对象

        Servlet servlet = (Servlet) Class.forName(servletClazz).newInstance();

        // 处理请求

        servlet.service(request, response);

        this.response.pushToClient(code);

        this.socket.close();

    } catch (Exception e) {

        e.printStackTrace();

    }

    

}

五、总结

手写 web 容器的内容到此结束。

文章中主要介绍编写 web 容器的大致流程,代码中还有许多地方需要考虑(过滤器、监听器、日志打印等)和优化,仅作为笔者对 web 容器的理解与分享,并不是为了教读者重复造轮子。

学习东西要知其然,更要知其所以然。

六、源码