一、背景

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

这个问题将在本篇解决。

二、涉及知识

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

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

三、封装控制器

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

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

# 3.1 抽离控制器

创建一个父类控制器:

  1. public class Servlet {
  2. public void service(Request request, Response response) {
  3. doGet(request,response);
  4. doPost(request,response);
  5. }
  6. protected void doGet(Request request, Response response) {
  7. }
  8. protected void doPost(Request request, Response response) {
  9. }
  10. }

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

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

  1. public class LoginServlet extends Servlet {
  2. @Override
  3. protected void doPost(Request request, Response response) {
  4. response.println("<!DOCTYPE html>")
  5. .println("<html lang=\"zh\">")
  6. .println(" <head> ")
  7. .println(" <meta charset=\"UTF-8\">")
  8. .println(" <title>测试</title>")
  9. .println(" </head> ")
  10. .println(" <body> ")
  11. .println(" <h3>Hello " + request.getParameter("username") + "</h3>")// 获取登陆名
  12. .println(" </body> ")
  13. .println("</html>");
  14. }
  15. }

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

# 3.2 创建 web 容器上下文

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

  1. /**
  2. * web 容器上下文
  3. * @author Light
  4. *
  5. */
  6. public class ServletContext {
  7. // 存储处理不同请求的 servlet
  8. // servletName -> servlet 子类
  9. private Map<String,Servlet> servletMap;
  10. // 存储请求 url 与 servlet 的对应关系
  11. // 请求 url -> servletName
  12. private Map<String,String> mappingMap;
  13. public ServletContext() {
  14. this.servletMap = new HashMap<String, Servlet>();
  15. this.mappingMap = new HashMap<String, String>();
  16. }
  17. public Map<String, Servlet> getServletMap() {
  18. return servletMap;
  19. }
  20. public void setServletMap(Map<String, Servlet> servletMap) {
  21. this.servletMap = servletMap;
  22. }
  23. public Map<String, String> getMappingMap() {
  24. return mappingMap;
  25. }
  26. public void setMappingMap(Map<String, String> mappingMap) {
  27. this.mappingMap = mappingMap;
  28. }
  29. }

# 3.3 创建配置类

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

  1. /**
  2. * 配置文件类
  3. * @author Light
  4. *
  5. */
  6. public class WebApp {
  7. private static ServletContext context;
  8. static {
  9. context = new ServletContext();
  10. Map<String,Servlet> servletMap = context.getServletMap();
  11. Map<String,String> mappingMap = context.getMappingMap();
  12. // 注册 登陆控制器
  13. servletMap.put("login", new LoginServlet());
  14. mappingMap.put("/login", "login");
  15. // 如有更多请求需要处理,在此配置对应的控制器即可
  16. }
  17. /**
  18. * 通过请求 url 获取对应的 Servlet 对象
  19. * @param url
  20. * @return
  21. */
  22. public static Servlet getServlet(String url) {
  23. if (url == null || "".equals(url.trim())) {
  24. return null;
  25. }
  26. String servletName = context.getMappingMap().get(url);
  27. Servlet servlet = context.getServletMap().get(servletName);
  28. return servlet;
  29. }
  30. }

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

  1. @Override
  2. public void run() {
  3. // 获取控制器
  4. Servlet servlet = WebApp.getServlet(this.request.getUrl());
  5. // 处理请求
  6. servlet.service(request, response);
  7. try {
  8. this.response.pushToClient(code);
  9. this.socket.close();
  10. } catch (IOException e) {
  11. e.printStackTrace();
  12. }
  13. }

四、解决硬编码问题

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

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

创建 web.xml 配置文件:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <web-app>
  3. <servlet>
  4. <servlet-name>login</servlet-name>
  5. <servlet-class>com.light.controller.LoginServlet</servlet-class>
  6. </servlet>
  7. <servlet-mapping>
  8. <servlet-name>login</servlet-name>
  9. <url-pattern>/login</url-pattern>
  10. </servlet-mapping>
  11. </web-app>

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

  1. /**
  2. * 封装 web.xml 中 <servlet> 配置信息
  3. * @author Light
  4. *
  5. */
  6. public class ServletXml {
  7. private String servletName;
  8. private String servletClazz;
  9. public String getServletName() {
  10. return servletName;
  11. }
  12. public void setServletName(String servletName) {
  13. this.servletName = servletName;
  14. }
  15. public String getServletClazz() {
  16. return servletClazz;
  17. }
  18. public void setServletClazz(String servletClazz) {
  19. this.servletClazz = servletClazz;
  20. }
  21. }
  22. /**
  23. * 封装 web.xml 中 <servlet-mapping> 配置信息
  24. * @author Light
  25. *
  26. */
  27. public class ServletMappingXml {
  28. private String servletName;
  29. private List<String> urlPattern = new ArrayList<>();
  30. public String getServletName() {
  31. return servletName;
  32. }
  33. public void setServletName(String servletName) {
  34. this.servletName = servletName;
  35. }
  36. public List<String> getUrlPattern() {
  37. return urlPattern;
  38. }
  39. public void setUrlPattern(List<String> urlPattern) {
  40. this.urlPattern = urlPattern;
  41. }
  42. }

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

  1. /**
  2. * xml文件解析器
  3. * @author Light
  4. *
  5. */
  6. public class WebHandler extends DefaultHandler{
  7. private List<ServletXml> servletXmlList;
  8. private List<ServletMappingXml> mappingXmlList;
  9. private ServletXml servletXml;
  10. private ServletMappingXml servletMappingXml;
  11. private String beginTag;
  12. private boolean isMapping;
  13. @Override
  14. public void startDocument() throws SAXException {
  15. servletXmlList = new ArrayList<>();
  16. mappingXmlList = new ArrayList<>();
  17. }
  18. @Override
  19. public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
  20. if (qName != null) {
  21. beginTag = qName;
  22. if ("servlet".equals(qName)) {
  23. isMapping = false;
  24. servletXml = new ServletXml();
  25. } else if ("servlet-mapping".equals(qName)){
  26. isMapping = true;
  27. servletMappingXml = new ServletMappingXml();
  28. }
  29. }
  30. }
  31. @Override
  32. public void characters(char[] ch, int start, int length) throws SAXException {
  33. if (this.beginTag != null) {
  34. String str = new String(ch,start,length);
  35. if(isMapping) {
  36. if("servlet-name".equals(beginTag)) {
  37. servletMappingXml.setServletName(str);
  38. } else if ("url-pattern".equals(beginTag)) {
  39. servletMappingXml.getUrlPattern().add(str);
  40. }
  41. } else {
  42. if("servlet-name".equals(beginTag)) {
  43. servletXml.setServletName(str);
  44. } else if ("servlet-class".equals(beginTag)) {
  45. servletXml.setServletClazz(str);
  46. }
  47. }
  48. }
  49. }
  50. @Override
  51. public void endElement(String uri, String localName, String qName) throws SAXException {
  52. if (qName != null) {
  53. if ("servlet".equals(qName)) {
  54. this.servletXmlList.add(this.servletXml);
  55. } else if ("servlet-mapping".equals(qName)){
  56. this.mappingXmlList.add(this.servletMappingXml);
  57. }
  58. }
  59. this.beginTag = null;
  60. }
  61. public List<ServletXml> getServletXmlList() {
  62. return servletXmlList;
  63. }
  64. public List<ServletMappingXml> getMappingXmlList() {
  65. return mappingXmlList;
  66. }
  67. }

改造 ServletContext 类:

  1. // 存储处理不同请求的 servlet
  2. // servletName -> servlet 子类的全名
  3. private Map<String,String> servletMap;

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

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

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

  1. /**
  2. * 配置文件类
  3. * @author Light
  4. *
  5. */
  6. public class WebApp {
  7. private static ServletContext context;
  8. static {
  9. context = new ServletContext();
  10. Map<String,String> servletMap = context.getServletMap();
  11. Map<String,String> mappingMap = context.getMappingMap();
  12. try {
  13. SAXParserFactory factory = SAXParserFactory.newInstance();
  14. SAXParser sax = factory.newSAXParser();
  15. WebHandler handler = new WebHandler();
  16. // 读取和解析 xml 文件
  17. sax.parse(Thread
  18. .currentThread()
  19. .getContextClassLoader()
  20. .getResourceAsStream("com/light/server/web.xml"),
  21. handler);
  22. // 注册 servlet
  23. List<ServletXml> servletXmlList = handler.getServletXmlList();
  24. for (ServletXml servletXml : servletXmlList) {
  25. servletMap.put(servletXml.getServletName(), servletXml.getServletClazz());
  26. }
  27. // 注册 mapping
  28. List<ServletMappingXml> mappingXmlList = handler.getMappingXmlList();
  29. for (ServletMappingXml mapping : mappingXmlList) {
  30. List<String> urls = mapping.getUrlPattern();
  31. for (String url : urls) {
  32. mappingMap.put(url, mapping.getServletName());
  33. }
  34. }
  35. } catch (Exception e) {
  36. e.printStackTrace();
  37. }
  38. }
  39. /**
  40. * 通过请求 url 获取对应的 Servlet 对象
  41. * @param url
  42. * @return
  43. */
  44. public static String getServletClazz(String url) {
  45. if (url == null || "".equals(url.trim())) {
  46. return null;
  47. }
  48. String servletName = context.getMappingMap().get(url);
  49. String servletClazz = context.getServletMap().get(servletName);
  50. return servletClazz;
  51. }
  52. }

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

  1. @Override
  2. public void run() {
  3. try {
  4. // 获取控制器包名
  5. String servletClazz = WebApp.getServletClazz(this.request.getUrl());
  6. // 通过反射实例化控制器对象
  7. Servlet servlet = (Servlet) Class.forName(servletClazz).newInstance();
  8. // 处理请求
  9. servlet.service(request, response);
  10. this.response.pushToClient(code);
  11. this.socket.close();
  12. } catch (Exception e) {
  13. e.printStackTrace();
  14. }
  15. }

五、总结

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

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

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

六、源码