简单服务器开发(九)Web服务器和WebApp解耦合
之前的开发步骤:简单服务器开发
分析之前的代码,LoginServlet 类是 JavaWeb 程序员开发的,而 HandlerRequest 类是服务器开发人员开发的,在服务器中的代码关心了具体的 Servlet 类,显然服务器的程序和 JavaWeb 程序产生了依赖,具有高强度的耦合,实际上对于 Web 服务器来说,根本就不知道 Web 应用中有一个 LoginServlet 类,上面的程序中还使用了“new LoginServlet();”,这显然是错误的。
另外在之前的 Web 服务器程序中编写了具体的请求路径/oa/login,这显然是不合理的,对于 Web 服务器来说浏览器客户端发送的请求是未知的。但是我们知道浏览器发送的请求路径/oa/login 是和底层 WebApp 中的 LoginServlet 存在映射关系/绑定关系的。而这种关系的指定必须由 WebApp 的开发人员来指定,我相信大家此时应该想到了“配置文件”,在配置文件中指定请求 URI 和对应要执行的 Servlet。该配置文件的编写由 WebApp程序员来完成,但是该文件由 Web 服务器开发人员读取并解析,所以该文件的名字、该文件的存放位置、该文件中所编写的配置元素都不能随意编写,必须提前制定好一个规范,那么这个规范由谁来制定呢?当然是由 SUN 公司来负责制定。
SUN 制定的配置文件规范:
A、在配置文件 web.xml 中描述请求 URI 和对应的 Servlet 类之间的关系
B、web.xml 文件统一存放到 WebAppRoot/WEB-INF/ 目录下(WebAppRoot是app的根目录)
C、web.xml 文件中采用如下的标签描述请求 URI 和对应 Servlet 类之间的关系
<?xml version="1.0" encoding="UTF-8"?> <web-app> <servlet> <servlet-name>ServletName</servlet-name> <servlet-class>Servlet完整类名</servlet-class> </servlet> <servlet-mapping> <servlet-name>ServletName</servlet-name> <url-pattern>URL</url-pattern> </servlet-mapping> </web-app>
servlet-mapping:
如果url-pattern定义的是路径,那么以后所有对这个路径下资源的请求都会由servlet-name中定义的servlet处理;
如果url-pattern定义的是资源格式例如*.do等,那么对于所有符合这种格式的资源的请求都由指定的servlet处理。
(1)WebApp开发人员
A、在OA应用下创建WEB-INF文件夹,并且在该文件夹下创建web.xml
B、在org.zda.oa.servlet下创建LoginServlet类,处理登录信息
注意:一个web.xml中可能有多个servlet。本例中的web.xml如下:
<?xml version="1.0" encoding="UTF-8"?> <web-app> <servlet> <servlet-name>loginServlet</servlet-name> <servlet-class>org.zda.oa.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>loginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping> <servlet> <servlet-name>userSaveServlet</servlet-name> <servlet-class>org.bjpowernode.oa.servlet.UserSaveServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>userSaveServlet</servlet-name> <url-pattern>/user/save</url-pattern> </servlet-mapping> </web-app>
(2)Web服务器开发人员
A、服务器开发者负责解析web.xml文件
a 、通过 Dom4j+xpath 解析 web.xml
B、在Web 服务器启动阶段解析web.xml文件
web.xml 文件中主要配置了请求 URI 和对应的 Servlet 完整类名,请求 URI 更像一个 Map 集合的 key,对应的 Servlet 完整类名更像一个 Map 集合的 value,所以我们决定采用一个Map集合存储解析出来的数据。在浏览器发送请求的时候再去解析web.xml文件时有点晚了,所以我们决定在 Web 服务器启动阶段解析 web.xml 文件。
package com.zda.httpserver.core; import java.io.File; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; /** * 解析服务器中的web.xml配置文件 * @author zda * @version 1.0 * @since 1.0 * */ public class WebParser { public static Map<String,Map<String,String>> servletMaps = new HashMap<String,Map<String,String>>(); /** * 解析服务器所有应用的web.xml * @param webAppNames 服务器中所有应用的名称 * @throws DocumentException */ public static void parser(String[] webAppNames) throws DocumentException{ for(String webAppName:webAppNames){ Map<String,String> servletMap = parser(webAppName); servletMaps.put(webAppName, servletMap); } } /** * 解析单个应用的web.xml配置文件 * @param webAppName 应用名称 * @return servletMap<String,String> * @throws DocumentException */ private static Map<String,String> parser(String webAppName) throws DocumentException{ //获取web.xml的路径 String webPath = webAppName + "/WEB-INF/web.xml"; //创建解析器 SAXReader saxReader = new SAXReader(); //通过解析器的read方法将配置文件读取到内存中,生成一个Document【org.dom4j】对象树 Document document = saxReader.read(new File(webPath)); //获取servlet节点元素: web-app -> servlet List<Element> servletNodes = document.selectNodes("/web-app/servlet"); //创建一个servletInfoMap集合,将servlet-name和servlet-class的值分别当作key和value存放到该集合中 Map<String,String> servletInfoMap = new HashMap<String,String>(); //开始遍历servletNodes for(Element servletNode:servletNodes){ //获取servlet-name节点元素对象 Element servletNameElt = (Element) servletNode.selectSingleNode("servlet-name"); //获取servletNameElt节点元素对象的值 String servletName = servletNameElt.getStringValue(); //获取servlet-class节点元素对象 Element servletClassElt = (Element) servletNode.selectSingleNode("servlet-class"); //获取servletClassElt节点元素对象的值 String servletClassName = servletClassElt.getStringValue(); //将servletName和servletClassName分别当作key和value存放到servletInfoMap集合中 servletInfoMap.put(servletName, servletClassName); } //获取servlet-mapping节点元素对象: web-app -> servlet-mapping List<Element> servletMappingNodes = document.selectNodes("/web-app/servlet-mapping"); //创建一个servletMappingInfoMap集合,将servlet-name和url-pattern节点元素对象的值分别当作key和value存放到该map集合中 Map<String,String> servletMappingInfoMap = new HashMap<String,String>(); //开始遍历servletMappingNodes for(Element servletMappingNode:servletMappingNodes){ //获取servlet-name节点元素对象 Element servletNameElt = (Element) servletMappingNode.selectSingleNode("servlet-name"); //获取servletNameElt节点元素对象的值 String servletName = servletNameElt.getStringValue(); //获取url-pattern节点元素对象 Element urlPatternElt = (Element) servletMappingNode.selectSingleNode("url-pattern"); //获取urlPatternElt节点元素对象的值 String urlPattern = urlPatternElt.getStringValue(); //将servletName和urlPattern分别当作key和value存放到servletMappingInfoMap集合中 servletMappingInfoMap.put(servletName, urlPattern); } //获取servletInfoMap或者servletMappingInfoMap任何一个key值的集合 Set<String> servletNames = servletInfoMap.keySet(); //创建一个servletMap集合,将servletMappingInfoMap的value和servletInfoMap的value分别当作key和value存放到该map集合中 Map<String,String> servletMap = new HashMap<String,String>(); //开始遍历servletNames for(String servletName:servletNames){ //获取servletMappingInfoMap集合中的value:urlPattern String urlPattern = servletMappingInfoMap.get(servletName); //获取servletInfoMap集合中的value:servletClass String servletClassName = servletInfoMap.get(servletName); //将urlPattern和servletClassName分别当作key和value存放到servletMap集合中 servletMap.put(urlPattern, servletClassName); } return servletMap; } }
解析出来servletClassName,在HandlerRequest里反射出该业务处理类。但是,怎么调用里面的方法?
SUN公司制定了制订了Servlet接口,所有的WebApp的业务处理类必须实现此接口,由服务器开发人员调用
package javax.servlet; /** * 由SUN公司制定的Servlet接口规范,该接口由web服务器开发人员来调用,由webApp开发人来实现 * @author SUN公司 * @version 1.0 * @since 1.0 * */ public interface Servlet { //处理业务的核心方法 void service(); }
处理请求:
package com.zda.httpserver.core; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.util.Map; import javax.servlet.Servlet; import com.zda.httpserver.util.Logger; /** * 处理客户端请求 * @author zda * @version 1.0 * @since 1.0 */ public class HandlerRequest implements Runnable { public Socket clientSocket; public HandlerRequest(Socket clientSocket){ this.clientSocket = clientSocket; } @Override public void run() { //处理客户端请求 BufferedReader br = null; PrintWriter out = null; Logger.log("httpserver thread: " + Thread.currentThread().getName()); try { //接收客户端消息 br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); //获取响应流对象 out = new PrintWriter(clientSocket.getOutputStream()); //获取请求协议的请求行 String requestLine = br.readLine();// GET /oa/index.html HTTP/1.1 //获取URI -> 请求行(requestLine) -> 请求方式 URI 请求协议版本号 -> 三者之间是通过一个空格进行连接 String requestURI = requestLine.split(" ")[1];//{"GET","/oa/index.html","HTTP/1.1"} System.out.println(requestURI); //判断用户请求是否为一个静态页面:以.html或.htm结尾的文件叫作html页面 if(requestURI.endsWith(".html") || requestURI.endsWith(".htm")){ //处理静态页面的方法 responseStaticPage(requestURI,out); }else{//动态资源:java程序,业务处理类 //requestURI: /oa/login?username=zhangsan&password=111 //requestURI: /oa/login String servletPath = requestURI; //判断servletPath是否包含参数 if(servletPath.contains("?")){ servletPath = servletPath.split("[?]")[0];// /oa/login } /*if("/oa/login".equals(servletPath)){ LoginServlet loginServlet = new LoginServlet(); loginServlet.service(); }*/ //获取应用的名称:oa 在 uri里:/oa/login String webAppName = servletPath.split("[/]")[1]; //获取servletMaps集合中的value值->servletMap -> key:urlPattern value:servletClassName Map<String,String> servletMap = WebParser.servletMaps.get(webAppName); //获取servletMap集合中的key值 -> 存在于uri中/oa/login /oa/use/xxx/xxx/xxx/xxx String urlPattern = servletPath.substring(1 + webAppName.length()); //获取servletClassName String servletClassName = servletMap.get(urlPattern); //判断该业务处理的Servlet类是否存在 if(servletClassName != null){ //通过反射机制创建该业务处理类 Class c = Class.forName(servletClassName); Object obj = c.newInstance(); //这个时候,服务器开发人员不知道如何调用servlet业务处理类里的方法了? //SUN站了出来,制订了Servlet接口,所有的WebApp的业务处理类必须实现此接口,由服务器开发人员调用 Servlet servlet = (Servlet)obj; servlet.service(); }else{//找不到该业务处理类:404 //404找不到资源 StringBuilder html = new StringBuilder(); html.append("HTTP/1.1 404 NotFound\n"); html.append("Content-Type:text/html;charset=utf-8\n\n"); html.append("<html>"); html.append("<head>"); html.append("<title>404-错误</title>"); html.append("<meta content='text/html;charset=utf-8'/>"); html.append("</head>"); html.append("<body>"); html.append("<center><font size='35px' color='red'>404-Not Found</font></center>"); html.append("</body>"); html.append("</html>"); out.print(html); } } //强制刷新 out.flush(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); }finally{ //关闭资源 if(br != null){ try { br.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(clientSocket != null){ try { clientSocket.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } /** * 处理静态页面 * @param requestURI 请求URI * @param out 响应流对象 */ public void responseStaticPage(String requestURI, PrintWriter out) { //requestURI:/oa/index.html //静态页面的路径:oa/index.html String htmlPath = requestURI.substring(1); BufferedReader br = null; try { //读取页面 br = new BufferedReader(new FileReader(htmlPath)); StringBuilder html = new StringBuilder(); //拼接响应信息 html.append("HTTP/1.1 200 OK\n"); html.append("Content-Type:text/html;charset=utf-8\n\n"); String temp = null; while((temp = br.readLine()) != null){ html.append(temp); } //输出html out.print(html); } catch (FileNotFoundException e) { //404找不到资源 StringBuilder html = new StringBuilder(); html.append("HTTP/1.1 404 NotFound\n"); html.append("Content-Type:text/html;charset=utf-8\n\n"); html.append("<html>"); html.append("<head>"); html.append("<title>404-错误</title>"); html.append("<meta content='text/html;charset=utf-8'/>"); html.append("</head>"); html.append("<body>"); html.append("<center><font size='35px' color='red'>404-Not Found</font></center>"); html.append("</body>"); html.append("</html>"); out.print(html); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
在服务器启动时解析服务器中包含的web.xml文件
package com.zda.httpserver.core; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import com.zda.httpserver.util.Logger; /** * httpserver程序主入口 * @author zda * @version 1.0 * @since 1.0 * */ public class BootStrap { /** * 主程序 * @param args */ public static void main(String[] args) { //程序入口 start(); } /** * 主程序入口 */ public static void start() { ServerSocket serverSocket = null; Socket clientSocket = null; Thread.currentThread().setName("MAIN"); try { Logger.log("httpserver start"); //获取当前时间 long start = System.currentTimeMillis(); //解析服务中包含的web.xml配置文件 WebParser.parser(new String[] {"oa"}); //获取系统端口号 int port = ServerParser.getPort(); Logger.log("httpserver-port: " + port); //创建服务器套接字,并且绑定端口号:8080 serverSocket = new ServerSocket(port); //获取结束时间 long end = System.currentTimeMillis(); Logger.log("httpserver started: " + (end-start) + " ms"); while(true){ //开始监听网络,此时程序处于等待状态,等待接收客户端的消息 clientSocket = serverSocket.accept(); /*//接收客户端消息 br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); String temp = null; while((temp = br.readLine()) != null){ System.out.println(temp); }*/ new Thread(new HandlerRequest(clientSocket)).start(); } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); }finally{ //关闭资源 /*if(br != null){ try { br.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(clientSocket != null){ try { clientSocket.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }*/ if(serverSocket != null){ try { serverSocket.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } }