JavaWeb

157 阅读1小时+

前置

系统架构有哪些?

  1. B/S架构 (浏览器/服务器)
    • 如baidu.com,升级方便,只需要升级服务端即可,不需要安装特定的客户端软件,但是速度慢,体验差,不安全(所有数据都在服务器上)
  2. C/S架构 (客户端/服务器)
    • 如QQ电脑端,需要安装客户端软件,速度快,安全,体验好,服务器压力小,但是升级维护比较麻烦

没有哪个好哪个不好之说,不同的系统架构在不同的业务场景下有不同的使用场景

web服务器软件有哪些?

  • Tomcat (WEB服务器) java语言写的,运行必须有jre
  • jetty (WEB服务器)
  • JBOSS (应用服务器)
  • WebLogic (应用服务器)
  • WebSphere (应用服务器)

应用服务器和WEB服务器的关系?

  • 应用服务器实现了JavaEE的所有规范(13种)
  • WEB服务器只实现了JavaEE的Servlet + JSP两个核心规范
  • 可以看出应用服务器是包含WEB服务器的

Tomcat

  • tomcat.apache.org/

  • Tomcat的目录

    • bin 存放Tomcat服务器的命令文件,如启动,关闭Tomcat
    • conf 配置文件存放目录,如server.xml可以配置端口号,默认8080
    • lib Tomcat服务器的核心程序,都是jar包,里面都是class文件
    • logs 日志目录
    • temp Tomcat服务器的临时目录,放临时文件
    • webapps 该目录存放大量的webapp (web application: web应用)
    • work 用来存放jsp文件翻译之后的java文件及编译之后的class文件
  • 启动Tomcat

    bin目录下有个 startup.bat ,通过命令行输入startup.bat可以启动Tomcat服务器

    • 这里的bat文件是Windows专用的,bat是批处理文件,可以编写大量的windows的dos命令,然后执行bat就相当于执行dos命令

    • startup.sh 在Windows中无法执行,实在Linux系统中使用的,里面有大量的shell命令,然后执行sh文件就相当于执行shell命令

    命令行启动的前提是环境变量里有

    • JAVA_HOME 值是jdk的根目录

    • CATALINA_HOME 值是tomcat的根目录

    • 有根目录后要在Path里写 %某某_HOME%\bin

  • 关闭Tomcat

    通过命令行输入shutdown.bat可以关闭Tomcat服务器,不要输入shutdown,防止Windows关机

  • 如何测试是否成功启动Tomcat

    打开浏览器,输入 http://127.0.0.1:8080 或者 http://localhost:8080,8080是tomcat的端口号

实现一个基本的web应用(没有用到java小程序)

  1. 找到CATALINA_HOME(tomcat的根目录)的webapps目录

    所有的webapp都要放到webapps目录下,否则tomcat服务器找不到

  2. 在webapps目录下建一个子目录,起个名字就是web应用的名字

  3. 在建好的目录下放入资源文件,如index.html

  4. 启动Tomcat服务器

  5. 在地址栏输入http://127.0.0.1:8080/目录地址/资源名

  6. 这种网页是静态网页

浏览器的访问路径也可以是一个web应用里的java小程序,让它返回一组数据等等,一个servlet小程序对应一个浏览器上输入的路径

在B/S架构中,我们开发的java小程序连接了浏览器和数据库,写的webapp遵循servlet规范,使得其在Tomcat,jetty,JBOSS,WebLogic,WebSphere上都能用,由此引出下面的servlet规范,规范的作用: 使得WEB Server和webapp解耦合(sun公司制定)

浏览器和WEB Server之间的协议是HTTP超文本传输协议(w3c制定)

webapp和DB Server之间有一套规范: JDBC规范(sun公司指定)

作为java程序员,只需要编写一个类,实现servlet接口,将编写的类配置到配置文件中,在配置文件中指定请求路径和类名的关系,tomcat会根据反射机制,从浏览器发过来的路径中找到对应的类名,去创建类

Servlet

开发一个B/S结构的系统,其实就是开发网站,Java做WEB开发就是JavaWEB开发,JavaWEB开发最核心的规范:Servlet(Server Applet:服务器端的java小程序)

JavaEE包括很多种规范(13种),Servlet就是JavaEE规范之一,学Servlet实际上在学java

根据servlet规范,文件放的位置,文件的命名,目录结构,java小程序的位置等等都得符合规范,这样的webapp才能在各种服务器软件中部署,规范包括

  1. 规范了哪些接口
  2. 规范了哪些类
  3. 规范了一个web应用中应该有哪些配置文件
  4. 规范了一个web应用中配置文件的名字
  5. 规范了一个web应用中配置文件存放的路径
  6. 规范了一个web应用中配置文件的内容
  7. 规范了一个合法有效的web应用它的目录结构应该是怎样的
  8. ......

手动开发一个带有Servlet(java小程序)的webapp

步骤

  1. 第一步 Tomcat根目录里在webapps目录下新建一个目录,起名(webapp的名字),比如银行项目,可以创建一个目录bak,办公系统可以创建一个目录oa,注意:该目录就是这个webapp的根

  2. 第二步 在webapp的根下,新建一个目录 WEB-INF ,这个名字是Servlet规定好的,必须的

  3. 第三步 在WEB-INF目录下,新建一个目录 classes ,这个名字是Servlet规定好的,必须的,全部小写,这个目录下一定存放的是Java程序编译之后的class文件(字节码文件)

  4. 第四步 在WEB-INF目录下,新建一个目录 lib ,注意,这个目录不是必须的,但如果一个webapp需要第三方的jar包的话,这个jar包要放到这个lib目录下,这个目录的名字也不能随意编写,必须是全部小写的lib,例如Java语言连接数据库需要数据库的驱动jar包,那么这个jar包就一定要放到lib目录下,这是Servlet规范中规定的。

  5. 第五步 在WEB-INF目录下,新建一个文件 web.xml ,注意,这个文件是必须的,这个文件名必须叫做web.xml,这个文件必须放在这里,一个合法的webapp里web.xml文件是必须的,这个web.xml文件就是一个配置文件,在这个配置文件中描述了请求路径和Servlet类之间的对照关系,这个文件最好从其他的webapp中拷贝,最好别手写,没必要,复制粘贴,内容大概是这样

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
             version="4.0">
        <servlet>
            <servlet-name>studentServlet</servlet-name> 
            <servlet-class>com.javastudy.javaweb.servlet.StudentServlet</servlet-class> 3.该servlet的包名
        </servlet>
        <servlet-mapping>
            <servlet-name>studentServlet</servlet-name> 2.浏览器请求的路径对应的servlet
            <url-pattern>/servlet/student</url-pattern> 1.浏览器请求的路径
        </servlet-mapping>
    </web-app>
    
  6. 第六步 编写一个Java小程序,这个小Java程序也不能随意开发,这个小Java程序必须实现Servlet接口(注意引入的包版本)

    • 这个Servlet接口不在JDK当中,因为Servlet不是JavaSE了,Servlet属于JavaEE,是另外的一套类库
    • Servlet接口(Servlet.class文件)是Oracle提供的,(最原始的是sun公司提供的)
    • Servlet接口是JavaEE的规范中的一员
    • Tomcat服务器实现了Servlet规范,所以Tomcat服务器也需要使用Servlet接口,Tomcat服务器中应该有这个接口,Tomcat服务器的CATALINA_HOME\lib目录下有一个servlet-api.jar,解压这个servlet-api.jar之后,你会看到里面有一个Servlet.class文件
    • 注意,编写这个Java小程序的时候,Java源代码你愿意在哪里就在哪里,位置无所谓,你只需要将Java源代码编译之后的class文件放到classes目录下即可
    public class HelloServlet implements Servlet {
        public void init(ServletConfig config) throws ServletException {
        }
    
        // 不用自己写main方法了,创建HelloServlet对象,调用service方法都是tomcat在做,我们只需要完成这个方法就行了 
        public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
            System.out.println("My first servlet!");
        }
    
        public void destroy() {
        }
    
        public String getServletInfo() {
            return "";
        }
    
        public ServletConfig getServletConfig() {
            return null;
        }
    }
    
  7. 第七步 编译我们写的java小程序,想javac编译通过,就注意将tomcat/lib下的servlet-api.jar添加到CLASSPATH中,编译得到字节码文件

  8. 第八步 将得到的xxx.class字节码文件拷贝到WEB-INF\classes目录下

  9. 第九步 在web.xml文件中编写配置信息,让"请求路径"和"servlet类名"关联在一起,这一步的专业术语叫:在web.xml文件中注册servlet类

    <web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd" version="5.0" metadata-complete="true">
        
        <!-- servlet描述信息 -->
        <!-- 任何一个servlet都对应一个servlet-mapping -->
        <servlet>
            <servlet-name>xxx</servlet-name>
            <!-- 这个位置必须是带有包名的全限定类名 -->
        	<servlet-class></servlet-class>
        </servlet>
        
        <!-- servlet映射信息 -->
        <servlet-mapping>
            <!-- 名字和上面的一样,可以随便写 -->
            <servlet-name>xxx</servlet-name>
            <!-- 这里需要一个路径,唯一要求是必须以 / 开始,可以随便写 -->
        	<url-pattern>/adf/adf/adf/sdfs/aaa</url-pattern>
        </servlet-mapping>
        
    </web-app>
    
  10. 第十步 启动Tomcat服务器,打开浏览器在地址栏输入urlhttp://localhost:8080/项目名/adf/adf/adf/sdfs/aaa,此时就可以在tomcat控制台看到输出的My first servlet!

怎样将一个信息输出到浏览器上?

需要使用ServletResponse接口:response

PrintWriter out = response.getWriter();

out.print("Hello servlet~");

这是一个输出流,负责将字符串输出到浏览器

这个流不需要我们刷新,也不需要我们关闭,都由tomcat来负责

也可以向浏览器响应一段html代码

response.setContentType("text/html");

PrintWriter out = response.getWriter();

out.print("<h1>hello</h1>");

一个合法的webapp目录结构如下


注意html文件只能放到WEB-INF目录外面

webapproot
	|-------WEB-INF
				|-------classes	(放字节码文件)
				|-------lib		(第三方jar包)
				|-------web.xml	(注册servlet)
	|-------html
	|-------css
	|-------js
	|-------image
	...

在Servlet中连接数据库

直接在Servlet的service方法中编写Java的JDBC代码即可,注意将驱动jar包放到lib目录

总结

浏览器发动请求,到最终的服务器调用servlet中的方法,大致是怎样的过程?

  • 用户输入url,或点击查链接http://localhost:8080/项目名/adf/adf/adf/sdfs/aaa
  • 然后tomcat收到请求,截取到路径/项目名/adf/adf/adf/sdfs/aaa
  • tomcat找到项目
  • tomcat服务器在web.xml文件中查找/adf/adf/adf/sdfs/aaa对应的servlet
  • tomcat通过反射机制,创建对应servlet的对象
  • tomcat服务器调用该对象的service方法

JavaEE8版本升级之后的"JavaEE9",不再是"JavaEE9"这个名字了,叫做JakartaEE9,JavaEE8的时候对应的Servlet类名是 javax.servlet.Servlet JakartaEE9的时候对应的Servlet类名是 jakarta.servlet.Servlet(包名都换了) 如果你之前的项目还是在使用javax.servlet.Servlet,那么你的项目无法直接部署到Tomcat10+版本上,你只能部署到Tomcat9-版本上,在Tomcat9以及Tomcat9之前的版本中还是能够识别javax.servlet这个包。

用IDEA开发Servlet程序

  1. 新建一个空工程,名为javaweb

  2. 新建模块(文件-->新建-->新模块)

    • 这里新建的一个普通的javase类型的模块,名为servlet01,会自动放在javaweb工程下
    • 这时servlet01模块就是一个普通的javase类型的模块,需要将它变成javaee模块(让它符合servlet规范,变成一个webapp模块),在模块上鼠标右键选择添加框架支持,选择JavaEE里的Web应用程序,并勾选上创建web.xml选项,点击确认
    • 选择webapp支持后,IDEA会自动生成一个符合Servlet规范的webapp目录结构,此时除了src文件夹还有个web文件夹,这个web文件夹就是之前手动写的例子的webapps里面的项目根目录的映射,即web文件夹就是webapp的根
  3. 此时就可以写一个servlet了,在src文件夹里写各种servlet类去实现Servlet接口

  4. 但是,此时会发现没有Servlet.class接口,因为此时是一个普通的JavaSE的模块,下方的外部库中只有jdk,而jdk里是没有Servlet接口的,Servlet属于JavaEE,这时就需要去tomcat安装目录下找lib文件夹,再找到里面的jsp-api.jar和servlet-api.jar,将他们加入到IDEA中去,(文件-->项目结构-->模块-->模块名-->依赖-->加号-->jar或目录-->添加tomcat安装目录下的lib文件夹里面的jsp-api.jar和servlet-api.jar-->确定),这时候就可以导入jakarta.servlet.Servlet包,成功地实现Servlet类了

  5. 实现Servlet的五个方法,将业务代码写在service方法中(包含连接数据库的操作)

    package com.javastudy.javaweb.servlet;
    
    import jakarta.servlet.*;
    
    import java.io.IOException;
    import java.io.PrintWriter;
    import java.sql.*;
    
    
    public class StudentServlet implements Servlet {
    
        @Override
        public void init(ServletConfig servletConfig) throws ServletException {
    
        }
    
        @Override
        public ServletConfig getServletConfig() {
            return null;
        }
    
        @Override
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
    
            servletResponse.setContentType("text/html");
            PrintWriter out = servletResponse.getWriter();
    
            Connection connection = null;
            ResultSet resultSet = null;
            PreparedStatement preparedStatement = null;
    
            try {
                Class.forName("com.mysql.cj.jdbc.Driver");
                String user = "root";
                String password = "root";
                String url = "jdbc:mysql://localhost:3306/servletstudy";
                connection = DriverManager.getConnection(url, user, password);
                String sql = "select * from student";
                preparedStatement = connection.prepareStatement(sql);
                resultSet = preparedStatement.executeQuery();
                while (resultSet.next()) {
                    String id = resultSet.getString("id");
                    String name = resultSet.getString("name");
                    out.print(id + name);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (resultSet != null) {
                    try {
                        resultSet.close();
                    } catch (SQLException e) {
                        throw new RuntimeException(e);
                    }
                }
                if (preparedStatement != null) {
                    try {
                        preparedStatement.close();
                    } catch (SQLException e) {
                        throw new RuntimeException(e);
                    }
                }
                if (connection != null) {
                    try {
                        connection.close();
                    } catch (SQLException e) {
                        throw new RuntimeException(e);
                    }
                }
    
            }
        }
    
        @Override
        public String getServletInfo() {
            return null;
        }
    
        @Override
        public void destroy() {
    
        }
    }
    
    
  6. 在WEB-INF目录下创建一个lib文件夹,将连接数据库的驱动jar包加入到lib目录下

  7. 在web.xml文件里完成该servlet类的注册(浏览器不同的请求路径会和不同的servlet对应起来)

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
             version="4.0">
    
        <servlet>
            <servlet-name>studentServlet</servlet-name>
            <servlet-class>com.javastudy.javaweb.servlet.StudentServlet</servlet-class>
        </servlet>
        <servlet-mapping>
            <servlet-name>studentServlet</servlet-name>
            <url-pattern>/servlet/student</url-pattern>
        </servlet-mapping>
    
    </web-app>
    
  8. 给index.html(放在WEB-INF外面)页面里写一个超链接,只要用户点了超链接就会跳到对应路径,也就会执行相应的servlet

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
         <!--这里的路径是 项目名/xml配置的路径-->
    	<a href="/stu/servlet/student">点我试试</a>
    </body>
    </html>
    
  9. 让IDEA工具关联Tomcat服务器,关联的过程中将webapp部署到Tomcat服务器中,点击运行按钮旁边的添加配置,点击加号,选择Tomcat服务器-本地,服务器-->点击应用程序服务器的配置,选择本地的tomcat_home,再选择合适的jre,再点击部署-->加号-->模块名,再看下面有个应用程序上下文,自定义项目名,点击应用

  10. 启动tomcat服务器,点击右上角的启动或debug启动tomcat,开发过程中建议使用debug模式启动

关于WEB-INF目录

在WEB-INF目录下新建了一个文件:welcome.html打开浏览器访问http://localhost:8080/servlet07/WEB-INF/welcome.html出现了404错误,注意,放在WEB-INF目录下的资源是受保护的,在浏览器上不能够通过路径直接访问,所以像HTML,CSS,JS,image等静态资源一定要放到WEB-INF目录之外

Servlet对象的生命周期

  • 生命周期就是servlet对象的创建到销毁的过程

  • servlet对象的创建销毁,对象上方法的调用,javaweb程序员是无权干预的,由tomcat服务器(web server)全权负责

  • Tomcat服务器(web server)又被称为WEB容器,WEB容器来管理servlet对象的死活

  • 我们自己new出来的Servlet对象是不受web容器管理的,web容器创建的servlet对象是放在集合当中的,只有该集合中的对象才能被web容器管理,所以自己创建的servlet对象是不受管理的

  • 默认情况下服务器在启动时servlet对象并不会被实例化,用户在请求时才会创建实例对象,特殊情况下如何让服务器启动时创建servlet对象呢?可以在web.xml的<servlet>里加一个<load-on-startup>2<load-on-startup>标签,中间的数字表示多个servlet启动时的优先级

  1. 用户在发送第一次请求时Servlet对象被实例化,马上执行init方法,且只会被调用1次,init方法被执行后又马上执行了service方法
  2. 发送第二次及后面的请求,只会执行service方法,此时的对象还是第一次创建的对象,可见servlet对象是单实例的,但是Servlet类并不符合单例模式,我们称之为假单例
  3. init方法只在用户发送第一次请求时被执行,只执行一次
  4. 只要用户发送请求,service方法必然会被Tomcat调用
  5. 关闭服务器时,destroy方法只会被调用一次,执行destroy方法时,servlet对象还在,可以在destroy方法里写一些关闭流,关闭连接等操作

servlet 对象就像人的一生

  1. 无参构造方法执行时,标志着出生了
  2. init方法执行时,标志着正在接受教育
  3. service方法执行时,开始工作了
  4. destroy方法执行时,标志着临终说遗言了

关于Servlet类中的方法执行了几次?

  1. 构造方法执行了一次
  2. init方法只执行了一次
  3. service方法用户发送几次请求就执行几次
  4. destroy方法只执行一次

注意:不建议在servlet类中写构造器,写无参构造器没事,只要一写有参构造器直接报错500,500一般是因为服务器的java程序出现了异常,人家给的规范中的init方法就是让你用来代替构造器写初始化代码的,把想写在构造器中的代码写道init方法中就好了,瞎写构造方法容易导致Servlet对象无法被实例化出来

Servlet类中,只有Service方法时最常用的,别的很少用,但是只要实现了Servlet接口就必须实现5个方法,全写上代码很丑,利用适配器模式设计一个抽象的适配器类,让该类去实现Servlet接口,它里面实现了5中方法,但是,只给service方法设置成了抽象方法,这时Servlet类就不要实现Servlet接口了,变为去继承这个适配器类且只用重写Service方法就行,这样一来Servlet方法就简洁很多,只有service方法了

好消息是,这个抽象的适配器类在Jakarta.servlet包里有,名字就叫GenericServlet,截至目前,以后写的servlet就去继承它就行了,不要再直接实现Servlet接口了

ServletConfig

ServletConfig是什么?是Servlet规范中的一员,是一个接口

现在写一个servlet,继承GenericServlet后,可以调用

ServletConfig servletConfig = this.getServletConfig();

得到一个servletConfig对象,ServletConfig是一个接口,谁去实现了这个接口呢?

org.apache.catalina.core.StandardWrapperFacade,可见,Tomcat服务器实现了ServletConfig接口

这里把Tomcat服务器换成jetty服务器,包名就不一样了,但是不同服务器都实现了ServletConfig这个规范

一个servlet对象中有一个ServletConfig对象,一对一

Tomcat服务器(WEB服务器)在创建Servlet对象时同时也创建了ServletConfig对象,也就是用户发送第一次请求时

在web.xml文件中,每个servlet对象的<servlet>标签中的配置信息,也就是下面的这一堆,会被包装到一个ServletConfig对象(配置信息对象)中

<servlet>
    <servlet-name>studentServlet</servlet-name>
    <servlet-class>com.javastudy.javaweb.servlet.StudentServlet</servlet-class>
</servlet>

ServletConfig对象有啥用呢?首先看看xml文件的<servlet>标签里还可以写啥

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0">
    <servlet>
        <servlet-name>s1</servlet-name>
        <servlet-class>com.javastudy.javaweb.servlet.Servlet01</servlet-class>

        <init-param>
            <param-name>driver</param-name>
            <param-value>com.mysql.cj.jdbc.Driver</param-value>
        </init-param>

        <init-param>
            <param-name>url</param-name>
            <param-value>jdbc:mysql://localhost:3306/servletstudy</param-value>
        </init-param>

        <init-param>
            <param-name>user</param-name>
            <param-value>root</param-value>
        </init-param>

        <init-param>
            <param-name>password</param-name>
            <param-value>root</param-value>
        </init-param>
    </servlet>

    <servlet-mapping>
        <servlet-name>s1</servlet-name>
        <url-pattern>/test</url-pattern>
    </servlet-mapping>
</web-app>

可以看到,<servlet>标签里还可以写<init-param>标签,可以定义一些初始化的参数,而这些参数将会被Tomcat封装到ServletConfig对象中,这时就可以通过ServletConfig对象身上的两个方法获取到这些初始化参数信息:

getInitParameterNames() 获取所有初始化参数的name,返回一个集合,遍历集合就行了

getInitParameter(初始化参数的名字) 传入初始化参数的名字,返回初始化参数的值

除此之外还有

getServletContext() 获取servletContext对象

getServletName() 获取servlet的name

想调用以上方法,可以不用获取ServletConfig对象,直接this调用也行,因为GenericServlet实现了ServletConfig接口

ServletContext对象

ServletContext对象又被称为应用域对象

ServletContext是什么?

context是上下文的意思,ServletContext对象其实对应的其实就是整个web.xml文件

举个例子,50个学生每一个都是一个Servlet,这50个学生在同一个教室,则ServletContext对象就是教室,ServletContext对象中的数据,所有servlet一定是共享的

是个接口,是Servlet规范中的一员,Tomcat服务器实现了ServletContext接口

ServletContext对象在web服务器启动的时候有Tomcat创建,在服务器关闭的时候销毁

Tomcat是一个容器,里面可以放多个webapp,对于一个webapp来说ServletContext对象只有一个

通过调用ServletConfig对象上的getServletContext方法获取ServletContext对象,直接this调用也行

在web.xml文件中,可以加<context-param>标签,将全局参数写在里面

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0">

    <!-- 以下<context-param>中的是全局初始化信息,所有的servlet共享,里面的配置信息可以在servletContext对象中获取 -->
    <context-param>
        <param-name>msg1</param-name>
        <param-value>666</param-value>
    </context-param>
    <context-param>
        <param-name>msg2</param-name>
        <param-value>888</param-value>
    </context-param>
    <context-param>
        <param-name>msg3</param-name>
        <param-value>999</param-value>
    </context-param>

    
    <!-- 分界线 -->
    
    <!-- 以下的是每个servlet单独的配置 -->
    <servlet>
        <servlet-name>servlet01</servlet-name>
        <servlet-class>com.javastudy.javaweb.servlet.Servlet01</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>servlet01</servlet-name>
        <url-pattern>/s1</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>servlet02</servlet-name>
        <servlet-class>com.javastudy.javaweb.servlet.Servlet02</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>servlet02</servlet-name>
        <url-pattern>/s2</url-pattern>
    </servlet-mapping>

</web-app>

这时ServletContext对象就派上用场了,它可以获取<context-param>中的全局参数,并且有方法可以读出其中的值ServletContext对象有哪些方法?

getInitParameterNames() 获取配置的name

getInitParameter(name) 根据name获取配置的值

getContextPath() 获取该项目的根路径,因为在java源代码当中有一些地方可能会需要应用的根路径,这个方法可以动态获取应用的根路径

getRealPath(String path) 里面传入一个文件路径,返回文件的绝对路径

log(Sting msg) 可以记录日志,记到tomcat根目录的log目录下,参数也可以再加一个异常类型

ServletContext对象还有一个名字:应用域(后面还会有请求域,会话域等等),如果一份数据是所有用户共享的,且数据量很小且很少修改,可以将其放入到servletContext对象中,为什么是共享的?因为是该对象一个webapp只有一个,为什么数据量要求要小?因为这个对象的生命周期比较长,从服务器开启到关闭,太占堆内存了影响服务器性能,为什么是很少修改的?因为共享的数据涉及到线程安全问题,所以放在servletContext对象中的数据一般都是只读的

数据量小,所有用户共享,又不修改,这样的数据放到ServletContext这个应用域当中,会大大提升效率,因为应用域相当于一个大缓存,放到缓存中的数据,下次再用的时候,不需要从数据库中再次获取,大大提升执行效率

说到缓存,有哪些缓存技术呢?

  1. 字符串常量池
  2. 整数型常量池-128~128,但凡是在这个范围当中的Integer对象不再创建新对象,直接从这个整数型常量池中获取,大大提升系统性能
  3. 数据库连接池(提前创建好N个连接对象,将连接对象放到集合当中,使用连接对象的时候,直接从缓存中拿,省去了连接对象的创建过程,效率提升
  4. 线程池(Tomcat服务器就是支持多线程的,所谓的线程池就是提前先创建好N个线程对象,将线程对象存储到集合中,然后用户请求过来之后,直接从线程池中获取线程对象,直接拿来用,提升系统性能
  5. 后期还会学习更多的缓存技术,例如:redis,mongoDB...

那么怎么增删取呢?

setAttribute(String name,Object value) 存

getAttribute(String name) 取

removeAttribute(String name) 删

但是,以后写的servlet不会继承GenericServlet,因为是B/S架构,这种系统属于HTTP超文本传输协议的,在Servlet规范当中,提供了一个类叫做HttpServlet,它是专门为HTTP协议准备的一个Servlet类,我们编写的Servlet类要继承HttpServlet(HttpServlet是HTTP协议专用的)使用HttpServlet处理HTTP协议更便捷,HttpServlet继承了GenericServlet

Servlet --> GenericServlet --> HttpServlet 爷孙三代

Tomcat服务器是支持多线程的,Tomcat服务器是在用户发送一次请求,就新建一个Threads线程对象吗?当然不是,实际上是在Tomcat服务器启动的时候,会先创建好N多个线程Thread对象,然后将线程对象放到集合当中,称为线程池,用户发送请求过来之后,需要有一个对应的线程来处理这个请求,这个时候线程对象就会直接从线程池中拿,效率比较高,所有的WEB服务器,或者应用服务器,都是支持多线程的,都有线程池机制

HttpServlet

HTTP协议

w3c制定的超文本传输协议,不但可以传普通文本,还有流媒体如声音,视频,图片等

B/S互相通讯都需要遵守HTTP协议,HTTP协议包括请求协议(B-->S)和响应协议(S-->B)

HTTP请求协议(4部分)

  • 请求行
    • 请求方式(7种)
      • GET(常用)
      • POST(常用)
      • DELETE
      • PUT
      • HEAD
      • OPTIONS
      • TRACE
    • URI
      • URI:统一资源标识符,代表网络中某个资源的名字,无法定位资源
      • 它和URL有什么区别?
    • 协议版本号
  • 请求头
    • 请求的主机
    • 主机的端口
    • 浏览器的信息
    • 平台信息
    • cookie等信息
    • ...
  • 空白行
    • 用来分隔请求头和请求体
  • 请求体
    • 向浏览器发送的具体数据

HTTP响应协议(4部分)

  • 状态行
    • 协议版本号
    • 状态码(HTTP协议规定的响应状态号,不同响应结果对应不同号码)
      • 200表示请求响应成功,正常结束
      • 404表示访问的资源不存在,通常是因为要么是你路径写错了,要么是路径写对了,但是服务器中对应的资源并没有启动成功,总之404是前端错误
      • 405表示前端发送的请求方式和后端请求的处理方式不一致时发生,比如前端是POST请求,后端的处理方式按照GET方式进行处理时,发生405
      • 500表示服务器端的程序出现了异常,一般认为是服务器端的错误导致的
      • 以4开始的,一般是浏览器端的错误导致的
      • 以5开始的,一般是服务器端的错误导致的
    • 状态的描述信息
      • ok 表示正常成功结束
      • not found 表示资源找不到
  • 响应头
    • 响应的内容类型
    • 响应的内容长度
    • 响应的时间
    • ...
  • 空白行
    • 用来分隔响应头和响应体
  • 响应体
    • 响应体就是响应的正文,这些内容是一个长的字符串,这个字符串被浏览器渲染,解释并执行,最终展示出效果

怎样向服务器发送POST请求?

目前为止,只有一种情况可以发送POST请求:form表单中的method属性值为method="post",其他一律是get请求:即在浏览器地址栏输入url,敲回车和点击超链接标签都是get,或者form表单没有method或method="get"默认都为get

GET和POST请求有什么区别?

  • get请求在发送数据时,数据会挂在URI的后面,并且在URI后添加了一个?,?后面的就是数据,如www.bilibili.com/video/BV1Z3…

  • post请求在发送数据时,数据会在请求体中发送,不会显示到地址栏上

  • 不管发的是get或是post,发送的请求数据格式都是相同的,只是位置不同,get在请求行里,post在请求体里,且值都是name=value&name=value&name=value&name=value&name=value的格式

    name和value是什么?name就是h5标签name属性中的值,value就是h5标签value属性中的值

  • get请求只能发送普通的字符串,并且有长度限制,具体限制长度没有明确规范

  • post可以发任何类型的数据,包括普通字符串,流媒体如视频,声音,图片,没有长度限制

  • get请求在W3C中是这样说的:get请求比较适合从服务器端获取数据,post请求比较适合向服务器端传送数据

  • get请求是绝对安全的,因为get只是为了从服务器获取,不会对服务器造成威胁,而post是相对危险的,因为post是向服务器提交数据,如果这些数据通过后门的方式进入到服务器,服务器是很危险的,另外post是为了提交数据,所以一般拦截请求时,都会拦截post

  • get本身是安全的,不要用错了地方说get不好

  • get请求支持缓存,任何一个get请求最终的"响应结果"都会被浏览器缓存起来,在浏览器缓存当中 实际上,你只要发送get请求,览器做的第一件事都是先从本地浏览器缓存中找,找不到的时候才会去服务器上获取,这种缓存机制目的是为了提高用户的体验,有没有这样一个需求:我们不希望get请求走缓存,怎么办?怎么避免走缓存?我希望每一次这个get请求都去服务器上找资源,不想从本地浏览器的缓存中取,只要每一次get请求的请求路径不同即可,可以在路径的后面添加一个每时每刻都在变化的"时间戳",这样每一次的请求路径都不一样,浏览器就不走缓存了

  • post请求不支持缓存,post请求之后,服务器"响应的结果"不会被浏览器缓存起来,因为这个缓存没有意义

  • GET请求和POST请求如何选择,什么时候使用GET请求,什么时候使用POST请求? 怎么选择GET请求和POST请求呢?衡量标准是什么呢?看你这个请求是想获取服务器端的数据,还是想向服务器发送数据,如果你是想从服务器上获取资源,建议使用GET请求,如果你这个请求是为了向服务器提交数据,建议使用POST请求,大部分的form表单提交,都是post方式,因为form表单中要填写大量的数据,这些数据是收集用户的信息,一般是需要传给服务器,服务器将这些数据保存/修改等,如果表单中有敏感信息,还是建议适用post请求,因为get请求会回显敏感信息到浏览器地址栏上(如密码信息),做文件上传或要传的数据不是普通文本,一定是post请求,其他情况都可以使用get请求

在说HttpServlet之前先看看设计模式

设计模式就是某个问题的某个固定的解决方案,可以被重复使用

有哪些设计模式?

  • GoF设计模式:通常我们说的23种设计模式
    • 单例模式
    • 工厂模式
    • 代理模式
    • 门面模式
    • 责任链设计模式
    • 观察者模式
    • 模块方法设计模式
    • ...
  • JavaEE设计模式
    • DAO
    • DTO
    • VO
    • PO
    • pojo
    • ...
  • ...

其中,什么是模板方法设计模式?

在模板类的模板方法当中定义核心算法骨架,具体的实现步骤可以延迟到子类当中完成,模板类通常是一个抽象类,模板类当中的模板方法定义核心算法,这个方法通常是final的(但也可以不是final的),模板类当中的抽象方法就是不确定实现的方法,这个不确定怎么实现的事交给子类去做,HttpServlet类就是这样的

HttpServlet类中有doGet和doPost方法,我们就不用重写service方法了,直接重写doGet或者doPost,在里面写业务代码

我们写的servlet继承HttpServlet类之后,要是重写了service方法,再里面写业务代码,也行,但是会失去提示405错误的服务,因为正确的写法是不重写service方法,而是根据需求转而重写doGet或者doPost方法,只要前端发的请求比如get收到后一旦发现你所写的servlet类中没有doGet方法,就会去父类HttpServlet中执行doGet方法,而HttpServlet的doGet方法是用来提醒405的,405表示是前端的错误,发送的请求方式不对,和服务器不一致,不是服务器需要的请求方式,总之,只要HttpServlet类中的doGet或者doPost方法执行,必然405,但是不要为了避免报405而将doGet和doPost都写,这样虽然看着就不报405了,但是失去了405机制的意义

到现在,终于定下来了一个最终版的Servlet类的开发步骤:

  1. 编写一个servlet类,直接继承HttpServlet
  2. 重写doGet方法或者重写doPost方法,到底写谁,根据业务需求由javaweb程序员说了算
  3. 在doGet或doPost里写业务代码
  4. 将所写的servlet类配置到web.xml文件中去
  5. 部署

ServletTest.class

package com.javastudy.javaweb.servlet;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

public class ServletTest extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        PrintWriter writer = resp.getWriter();
        writer.print("<h1>测试代码</h1>");
    }
    
    // 或者写doPost,和doGet二选一
}

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0">

    <servlet>
        <servlet-name>Servlet1</servlet-name>
        <servlet-class>com.javastudy.javaweb.servlet.ServletTest</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Servlet1</servlet-name>
        <url-pattern>/test</url-pattern>
    </servlet-mapping>

</web-app>

关于web站点的欢迎页面

当访问webapp时没有指定任何资源路径,这时候会默认访问你设置好的欢迎页面

http://localhost:8080/webappname会跳到设置好的欢迎页面

在web.xml文件的web-app标签里写,注意该html在webapp的根下的话前面不用写/

要是该index.html在某个目录下,前面不用写/,直接写路径,它会自动从webapp的根下找

<welcome-file-list>
    <welcome-file>index.html</welcome-file>
</welcome-file-list>

注意,一个webapp是可以设置多个欢迎页的,约考上优先级越高,找不到继续向下找,如下所示

<welcome-file-list>
    <welcome-file>index1.html</welcome-file>
    <welcome-file>index2.html</welcome-file>
</welcome-file-list>

当我的文件名设置为index.html的时候,不需要在web.xml文件中进行配置欢迎页面,这是为什么? 这是因为小猫咪Tomcat服务器已经提前配置好了,实际上配置欢迎页面有两个地方可以配置,一个是在webapp内部的web.xml文件中(在这个地方配置的属于局部配置),另一个是在CATALINA_HOME/conf/web.xml文件中进行配置(在这个地方配置的属于全局配置)

欢迎页可以是个servlet吗?当然可以,将上面的index.html换成<url-pattern>里的自定义路径名就行

关于HttpServletRequest接口详解

public class ServletTest extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        PrintWriter writer = resp.getWriter();
        writer.print("<h1>测试代码</h1>");
    }
}

从上面代码看出,继承HttpServlet类之后,HttpServletRequest req, HttpServletResponse resp这两个参数显得尤为重要,有必要详细研究一下

  • HttpServletRequest 接口时Servlet规范中的一员
  • HttpServletRequest 接口的父接口是ServletRequest
  • Tomcat服务器(WEB服务器,WEB容器)实现了HttpServletRequest接口,还是说明了Tomcat服务器实现了Servlet规范,而对于我们Javaweb程序员来说,实际上不需要关心这个,我们只需要面向接口编程即可,我们关心的是HttpServletRequest接口中有哪些方法,这些方法可以完成什么功能!!!!
  • 实际上是用户发送请求的时候,遵循了HTTP协议,发送的是HTTP的请求协议,Tomcat服务器将HTTP协议中的信息以及数据全部解析出来,然后Tomcat服务器把这些信息封装到HttpServletRequest对象当中,传给了我们Javaweb程序员,Javaweb程序员面向HttpServletRequest接口编程,调用方法就可以从req获取到请求的信息了
  • req和resp对象的生命周期?request对象和response对象,一个是请求对象,一个是响应对象,这两个对象只在当前请求中有效,一次请求对应一个request,两次请求则对应两个request...
  • 前端一旦提交表单,传过来的数据是username=asdfasdf&age=22&aihao=chouyan这种形式的,思考一下收到后存在什么数据结构里比较好?当然是Map<String, String[]>里,因为传过来的键值对可能是一个key对应好几个不同的value,所以要用string[]数组来存值
  key					value
===================================
  username				{"adsf"}
  password				{"xcvxcvx"}
  aihao					{"抽烟","喝酒"}

注意:前端表单提交数据的时候,假设提交了120这样的数字,其实是以字符串"120"的方式提交的,所以服务器端获取到的一定是一个字符串的"120",而不是一个数字,前端永远提交的是字符串,后端获取的也永远是字符串

request对象有哪些常用的方法呢?

  1. getParameterMap();
  2. getParameterNames();
  3. getParameterValues();
  4. getParameter();
Map<String, String[]> parameterMap = req.getParameterMap();// 获取Map,用的不多
Enumeration<String> parameterNames = req.getParameterNames();// 获取Map中所有的key,用的不多
String[] parameterValues = req.getParameterValues(name);//根据key获取Map中的value(一维数组),用的不多
String parameter = req.getParameter(name);//根据key获取Map中的value的一维数组的第一个元素,这个最常用!!因为一维数组一般只有一个元素

request对象由被称为请求域对象

  1. 和之前的应用域对象相比,请求域对象要比应用域对象范围小很多,生命周期短很多

  2. 请求域对象也有三个方法

    setAttribute(String name,Object value)	存
    
    getAttribute(String name)	取
    
    removeAttribute(String name)	删
    

    请求域对象和应用域对象怎样选?

    个请求对象request对应一个请求域对象,一次请求结束之后,这个请求域就销毁了,所以尽量使用小的域对象,因为小的域对象占用的资源较少

题外话,如何一次请求让服务器执行两个servlet?

使用转发机制,即在第一个servlet里写如下代码

RequestDispatcher requestDispatcher = request.getRequestDispatcher("/test02");// xml里配置的第二个servlet的路径,注意,也可以是个资源,如html
requestDispatcher.forward(request, response);// 将request, response对象传给第二个servlet,第二个servlet接收到request, response

// 因为是同一个request, response对象,所以两个servlet算同一个请求

两个servlet怎样共享数据?

  1. 将数据放到ServletContext应用域当中当然是可以的,但是应用域范围太大,占用资源太多,不建议使用
  2. 可以将数据放到request域当中,然后AServlet转发到BServlet,保证AServlet和BServlet在同一次请求当中,这样就可以做到两个Servlet,或者多个Servlet共享同一份数据

关于request对象中两个非常容易混淆的方法

getAttribute(name") 获取请求域中绑定的数据,之前一定执行过request.setAttribute("name",new object(O)

getParameter("name") 获取用户在浏览器上提交的数据

request对象还有哪些常用的方法呢?

getRemoteAddr() 获取客户端的IP地址

setCharacterEncoding("UTF-8") 设置请求体的字符集,显然这个方法是处理POST请求的乱码问题,这种方式并不能解决get请求的乱码问题,Tomcat10之后,request请求体当中的字符集默认就是UTF-8,不需要设置字符集,不会出现乱码问题,Tomcat9前(包括9在内),如果前端请求体提交的是中文,后端获取之后出现乱码,怎么解决这个乱码?执行该方法就行,除了请求,在Tomcat9之前(包括9),响应response的中文也是有乱码的,怎么解决这个响应的乱码? response.setContentType("text/html;charset=UTF-8");在Tomcat10之后,包括10在内,响应中文的时候就不在出现乱码问题了,以上代码就不需要设置UTF-8了,get请求乱码问题怎么解决?get请求发送的时候,数据是在请求行上提交的,不是在请求体当中提交的,方案:修改CATALINA_HOME/conf/server.xml配置文件<Connector URIEncoding="UTF-8"/>注意,从Tomcat8之后,URIEncoding的默认值就是UTF-8,所以GET请求也没有乱码问题了

getContextPath() 动态获取webapp的根路径

getMethod() 获取请求方式

getRequestURI() 获取请求的URI

getServletPath() 获取servlet的路径

使用纯Servlet做一个单表的增删改查

  1. 在javaweb数据库中先创建一张表

    create table dept(
    	deptno int primary key,
    	dname varchar(255),
    	loc varchar(255)
    );
    insert into dept (deptno,dname,loc) values (10,'sell','beijing');
    insert into dept (deptno,dname,loc) values (20,'media','shanghai');
    insert into dept (deptno,dname,loc) values (30,'research','guangzhou');
    insert into dept (deptno,dname,loc) values (40,'technology','shenzhen');
    
  2. 准备一套html页面,确保页面间跳转是能成功的

    • 欢迎页面 index.html
    • 列表页面 list.html (以该页面为核心,展开其他操作)
    • 新增页面 add.html
    • 修改页面 edit.html
    • 详情页面 detail.html

    分析这个系统有哪些功能?

    • 查看部门列表
    • 新增部门
    • 删除部门
    • 查看部门信息
    • 修改部门
  3. 在IDEA中搭建开发环境

    • 创建一个webapp(依赖了jsp-api.jar和servlet-api.jar)

    • 向webapp中添加连接数据库的jar包(MySQL驱动)

      • 必须在WEB-INF目录下新建lib目录,然后将MySQL的驱动jar包拷贝到这个lib目录下,这个目录名必须叫做lib,全部小写
    • JDBC工具类,和对应的jdbc.properties配置文件,放在了src-resources目录下

      DBUtils

      package com.javastudy.oa.utils;
      
      import java.sql.*;
      import java.util.ResourceBundle;
      
      // JDBC的工具类
      public class DBUtils {
      
          // 静态变量,类加载时执行
          // 自上而下执行
      
          // 属性资源文件绑定
          private static ResourceBundle bundle = ResourceBundle.getBundle("resources.jdbc");
          // 根据属性配置文件key获取value
          private static String driver = bundle.getString("driver");
          private static String url = bundle.getString("url");
          private static String user = bundle.getString("user");
          private static String password = bundle.getString("password");
      
          static {
              // 注册驱动,只需要一次,放在静态代码块中,DBUtils类加载时调用
              try {
                  Class.forName(driver);
              } catch (ClassNotFoundException e) {
                  throw new RuntimeException(e);
              }
          }
      
          // 获取连接对象connection
          public static Connection getConnection() throws SQLException {
              // 获取连接
              return DriverManager.getConnection(url, user, password);
          }
      
          // 关闭资源
          public static void close(Connection conn, Statement stmt, ResultSet rs) throws SQLException {
              if (rs != null) {
                  rs.close();
              }
              if (stmt != null) {
                  stmt.close();
              }
              if (conn != null) {
                  conn.close();
              }
          }
      }
      

      jdbc.properties

      driver=com.mysql.jdbc.Driver
      url=jdbc:mysql://localhost:3306/javaweb
      user=root
      password=root
      
    • 将所有html页面拷贝到web目录下

  4. 实现功能

    • 我们应该怎么去实现一个功能呢?建议:你可以从后端往前端一步一步写,也可以从前端一步一步往后端写,都可以,但是千万要记住不要想起来什么写什么,你写代码的过程最好是程序的执行过程,也就是说,程序执行到哪里,你就写哪里,这样一个顺序流下来之后,基本上不会出现什么错误和意外

    • 第一步可以将前端的首页页面里查看部门列表按钮的a标签的跳转连接写成<a href="/oa/dept/list">展示所有部门</a>,因为用户最先点击的就这个超链接

      <!DOCTYPE html>
      <html lang="en">
        <head>
          <meta charset="UTF-8" />
          <meta http-equiv="X-UA-Compatible" content="IE=edge" />
          <meta name="viewport" content="width=device-width, initial-scale=1.0" />
          <title>欢迎</title>
        </head>
        <body>
          <h1 style="text-align: center">欢迎使用部门管理系统</h1>
          <hr />
          <a href="/oa/dept/list" style="display: block; text-align: center">展示所有部门</a>
        </body>
      </html>
      
    • 第二步,编写web.xml文件

      <?xml version="1.0" encoding="UTF-8"?>
      <web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
               version="5.0">
          <servlet>
              <servlet-name>list</servlet-name>
              <servlet-class>com.javastudy.oa.web.action.DeptListServlet</servlet-class>
          </servlet>
          <servlet-mapping>
              <servlet-name>list</servlet-name>
              <!--这里的路径不需要加项目名,前端页面里的需要-->
              <url-pattern>/dept/list</url-pattern>
          </servlet-mapping>
      </web-app>
      
    • 第三步,编写DeptListServlet这个类,继承HttpServlet类,然后重写doGet方法,因为前端超链接属于get请求

      package com.javastudy.oa.web.action;
      
      import jakarta.servlet.ServletException;
      import jakarta.servlet.http.HttpServlet;
      import jakarta.servlet.http.HttpServletRequest;
      import jakarta.servlet.http.HttpServletResponse;
      
      import java.io.IOException;
      
      public class DeptListServlet extends HttpServlet {
          @Override
          protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              
          }
      }
      
    • 第四步,在DeptListServlet类中的doGet方法中连接数据库,查询所有部门,动态展示部门列表

      分析list.html页面中那部分是固定死的,那部分是需要动态展示的,list.html页面中的内容所有的双引号要替换成单引号,因为writer.print("")这里有一个双引号,容易冲突,现在写完这个功能之后,你会有一种感觉,感觉开发很繁琐,只使用servlet写代码太繁琐了

      package com.javastudy.oa.web.action;
      
      import com.javastudy.oa.utils.DBUtils;
      import jakarta.servlet.ServletException;
      import jakarta.servlet.http.HttpServlet;
      import jakarta.servlet.http.HttpServletRequest;
      import jakarta.servlet.http.HttpServletResponse;
      
      import java.io.IOException;
      import java.io.PrintWriter;
      import java.sql.Connection;
      import java.sql.PreparedStatement;
      import java.sql.ResultSet;
      import java.sql.SQLException;
      
      public class DeptListServlet extends HttpServlet {
          @Override
          protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              // 获取应用的根路径
              String contextPath = req.getContextPath();
              
              // 设置响应的内容类型以及字符集,防止中文乱码问题
              resp.setContentType("text/html;charset=UTF-8");
              PrintWriter writer = resp.getWriter();
      
              writer.print("<!DOCTYPE html>");
              writer.print("<html lang='en'>");
              writer.print("  <head>");
              writer.print("    <meta charset='UTF-8' />");
              writer.print("    <meta http-equiv='X-UA-Compatible' content='IE=edge' />");
              writer.print("    <meta name='viewport' content='width=device-width, initial-scale=1.0' />");
              writer.print("    <title>部门列表</title>");
              writer.print("  </head>");
              writer.print("  <body>");
              writer.print("    <h1 style='text-align: center'>部门列表</h1>");
              writer.print("    <hr />");
              writer.print("    <table border='1px' align='center'>");
              writer.print("      <tr>");
              writer.print("        <th>序号</th>");
              writer.print("        <th>部门编号</th>");
              writer.print("        <th>部门名称</th>");
              writer.print("        <th>操作</th>");
              writer.print("      </tr>");
              // 以上内容是固定的
      
              // 连接数据库,查询所有部门
              Connection connection = null;
              PreparedStatement preparedStatement = null;
              ResultSet resultSet = null;
              try {
                  // 获取连接
                  connection = DBUtils.getConnection();
                  // 编写sql语句,获取预编译的数据库操作对象
                  String sql = "select * from dept";
                  preparedStatement = connection.prepareStatement(sql);
                  // 执行sql语句
                  resultSet = preparedStatement.executeQuery();
                  // 处理结果集
                  int i = 0;
                  while (resultSet.next()) {
                      String deptno = resultSet.getString("deptno");
                      String dname = resultSet.getString("dname");
                      String loc = resultSet.getString("loc");
                      // 以下是动态的
                      writer.print("      <tr>");
                      writer.print("        <td>" + (++i) + "</td>");
                      writer.print("        <td>" + deptno + "</td>");
                      writer.print("        <td>" + dname + "</td>");
                      writer.print("        <td>");
                      writer.print("          <a href=''>删除</a>");
                      writer.print("          <a href='edit.html'>修改</a>");
                      writer.print("          <a href='detail.html'>详情</a>");
                      writer.print("        </td>");
                      writer.print("      </tr>");
                  }
              } catch (SQLException e) {
                  e.printStackTrace();
              } finally {
                  try {
                      // 释放资源
                      DBUtils.close(connection, preparedStatement, resultSet);
                  } catch (SQLException e) {
                      e.printStackTrace();
                  }
              }
      
              // 以下内容是固定的
              writer.print("    </table>");
              writer.print("    <a href='add.html' style='text-align: center; display: block'>新增</a>");
              writer.print("  </body>");
              writer.print("</html>");
          }
      }
      
    • 第五步, 实现部门详情页

      建议:从前端往后端一步一步实现,首先要考虑的是,用户点击的是什么?用户点击的东西在哪里?

      找了半天,在java后端的servlet程序中找到了writer.print(" <a href='detail.html'>详情</a>");,明显要将里面的detail.html换成一个servlet路径,因为详情页的内容是要连接数据库的,所以肯定有java代码,即在一个servlet中,即将上边的连接写成:

      writer.print("          <a href='" + contextPath + "/dept/detail?deptno=" + deptno + "'>详情</a>");
      

      重点:向服务器提交数据的格式 url?name=value&name=value&name=value

      然后就可以再写一个servlet文件了,名为DeptDetailServlet,并且在web.xml文件中配置一下

      package com.javastudy.oa.web.action;
      
      import com.javastudy.oa.utils.DBUtils;
      import jakarta.servlet.ServletException;
      import jakarta.servlet.http.HttpServlet;
      import jakarta.servlet.http.HttpServletRequest;
      import jakarta.servlet.http.HttpServletResponse;
      
      import java.io.IOException;
      import java.io.PrintWriter;
      import java.sql.Connection;
      import java.sql.PreparedStatement;
      import java.sql.ResultSet;
      import java.sql.SQLException;
      
      public class DeptDetailServlet extends HttpServlet {
          @Override
          protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
      
              resp.setContentType("text/html;charset=UTF-8");
              PrintWriter writer = resp.getWriter();
      
              // 固定的内容
              writer.print("<!DOCTYPE html>");
              writer.print("<html lang='en'>");
              writer.print("  <head>");
              writer.print("    <meta charset='UTF-8' />");
              writer.print("    <meta http-equiv='X-UA-Compatible' content='IE=edge' />");
              writer.print("    <meta name='viewport' content='width=device-width, initial-scale=1.0' />");
              writer.print("    <title>部门详情</title>");
              writer.print("  </head>");
              writer.print("  <body>");
              writer.print("    <h1 style='text-align: center'>部门详情</h1>");
              writer.print("    <hr />");
              writer.print("    <div style='text-align: center;'>");
      
              // 获取部门编号
              // /oa/dept/detail?deptno=10
              // 前端提交的是10,但是接收到的是"10"字符串
              String deptno = req.getParameter("deptno");// 将接收到的部门编号保存到字符串中
      
              // 连接数据库,根据部门编号查询部门信息
              Connection connection = null;
              PreparedStatement preparedStatement = null;
              ResultSet resultSet = null;
              try {
                  connection = DBUtils.getConnection();
                  String sql = "select dname,loc from dept where deptno = ?";
                  preparedStatement = connection.prepareStatement(sql);
                  preparedStatement.setString(1, deptno);
                  resultSet = preparedStatement.executeQuery();
                  if (resultSet.next()) {
                      String dname = resultSet.getString("dname");
                      String loc = resultSet.getString("loc");
                      System.out.println(deptno);
                      System.out.println(dname);
                      System.out.println(loc);
      
                      // 动态内容
                      writer.print("<p>部门编号:" + deptno + "</p>");
                      writer.print("<p>部门名称:" + dname + "</p>");
                      writer.print("<p>部门位置:" + loc + "</p>");
                  }
              } catch (SQLException e) {
                  e.printStackTrace();
              } finally {
                  try {
                      DBUtils.close(connection, preparedStatement, resultSet);
                  } catch (SQLException e) {
                      e.printStackTrace();
                  }
              }
      
              // 固定的内容
              writer.print("      <input type='button' value='返回' onclick='window.history.back()'>");
              writer.print("    </div>");
              writer.print("  </body>");
              writer.print("</html>");
          }
      }
      

      上面的servlet对应的xml配置

      <servlet>
          <servlet-name>detail</servlet-name>
          <servlet-class>com.javastudy.oa.web.action.DeptDetailServlet</servlet-class>
      </servlet>
      <servlet-mapping>
          <servlet-name>detail</servlet-name>
          <url-pattern>/dept/detail</url-pattern>
      </servlet-mapping>
      
    • 第六步,删除部门

      package com.javastudy.oa.web.action;
      
      import com.javastudy.oa.utils.DBUtils;
      import jakarta.servlet.ServletException;
      import jakarta.servlet.http.HttpServlet;
      import jakarta.servlet.http.HttpServletRequest;
      import jakarta.servlet.http.HttpServletResponse;
      
      import java.io.IOException;
      import java.sql.Connection;
      import java.sql.PreparedStatement;
      import java.sql.ResultSet;
      import java.sql.SQLException;
      
      public class DeptDeleteServlet extends HttpServlet {
          @Override
          protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              // 根据部门编号,删除部门
              String deptno = req.getParameter("deptno");
              // 连接数据库,根据部门编号删除部门
              Connection connection = null;
              PreparedStatement preparedStatement = null;
              ResultSet resultSet = null;
              int count = 0;
              try {
                  connection = DBUtils.getConnection();
                  // 开启事务
                  connection.setAutoCommit(false);
                  String sql = "delete from dept where deptno = ?";
                  preparedStatement = connection.prepareStatement(sql);
                  preparedStatement.setString(1, deptno);
                  // 返回受影响的行数
                  count = preparedStatement.executeUpdate();
                  // 提交事务
                  connection.commit();
              } catch (SQLException e) {
                  try {
                      // 遇到异常回滚事务
                      assert connection != null;
                      connection.rollback();
                  } catch (SQLException ex) {
                      throw new RuntimeException(ex);
                  }
                  e.printStackTrace();
              } finally {
                  try {
                      DBUtils.close(connection, preparedStatement, resultSet);
                  } catch (SQLException e) {
                      e.printStackTrace();
                  }
              }
      
              // 判断是否删除成功
              if (count == 1) {
                  System.out.println("成功");
                  // 成功后跳回列表页面,这里用了转发,没有重定向
                  req.getRequestDispatcher("/dept/list").forward(req, resp);
              } else {
                  System.out.println("失败");
              }
          }
      }
      
    • 第七步,新增部门

      package com.javastudy.oa.web.action;
      
      import com.javastudy.oa.utils.DBUtils;
      import jakarta.servlet.ServletException;
      import jakarta.servlet.http.HttpServlet;
      import jakarta.servlet.http.HttpServletRequest;
      import jakarta.servlet.http.HttpServletResponse;
      
      import java.io.IOException;
      import java.sql.Connection;
      import java.sql.PreparedStatement;
      import java.sql.SQLException;
      
      public class DeptAddServlet extends HttpServlet {
          @Override
          protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              req.setCharacterEncoding("UTF-8");
      
              String deptno = req.getParameter("deptno");
              String dname = req.getParameter("dname");
              String loc = req.getParameter("loc");
      
      
              // 连接数据库,根据部门编号查询部门信息
              Connection connection = null;
              PreparedStatement preparedStatement = null;
              int count = 0;
              try {
                  connection = DBUtils.getConnection();
                  String sql = "insert into dept values (?,?,?)";
                  preparedStatement = connection.prepareStatement(sql);
                  preparedStatement.setString(1, deptno);
                  preparedStatement.setString(2, dname);
                  preparedStatement.setString(3, loc);
                  count = preparedStatement.executeUpdate();
      
              } catch (SQLException e) {
                  e.printStackTrace();
              } finally {
                  try {
                      DBUtils.close(connection, preparedStatement, null);
                  } catch (SQLException e) {
                      e.printStackTrace();
                  }
              }
              // 判断是否删除成功
              if (count == 1) {
                  System.out.println("成功");
                  // 成功后跳回列表页面,这里用了转发,没有重定向,注意,这里是doPost请求,给DeptListServlet转发,DeptListServlet是doGet请求,会出现405,解决方法是在DeptListServlet里再重写一个doPost,在doPost里调用doGet!!!!!
                  req.getRequestDispatcher("/dept/list").forward(req, resp);
              } else {
                  System.out.println("失败");
                  req.getRequestDispatcher("/dept/list").forward(req, resp);
              }
          }
      }
      

      add.html

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8"/>
          <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
          <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
          <title>新增部门</title>
      </head>
      <body>
      <h1 style="text-align: center">新增部门</h1>
      <hr/>
      <form action="/oa/dept/add" align="center" method="post">
          部门编号<input type="text" name="deptno"/><br/>
          部门名称<input type="text" name="dname"/><br/>
          部门位置<input type="text" name="loc"/><br/>
          <input type="submit" value="保存"/>
      </form>
      </body>
      </html>
      

      xml配置

      <!--添加部门-->
      <servlet>
          <servlet-name>add</servlet-name>
          <servlet-class>com.javastudy.oa.web.action.DeptAddServlet</servlet-class>
      </servlet>
      <servlet-mapping>
          <servlet-name>add</servlet-name>
          <url-pattern>/dept/add</url-pattern>
      </servlet-mapping>
      

      注意,最后保存成功之后,转发到/dept/list的时候,会出现405,为什么?

      1. 保存用的是post请求,底层要执行doPost方法
      2. 转发是一次请求,之前是post,之后还是post,因为它是一次请求
      3. /dept/list Servlet当中只有一个doGet方法
        • 怎么解决?两种方案 第一种 在/dept/list Servlet中添加doPost方法,然后在doPost方法中调用doGet 第二种 重定向,还没学
    • 跳转到修改部门界面,修改部门

          <!--跳转到编辑部门的页面-->
          <servlet>
              <servlet-name>edit</servlet-name>
              <servlet-class>com.javastudy.oa.web.action.DeptEditServlet</servlet-class>
          </servlet>
          <servlet-mapping>
              <servlet-name>edit</servlet-name>
              <url-pattern>/dept/edit</url-pattern>
          </servlet-mapping>
      
          <!--修改部门-->
          <servlet>
              <servlet-name>modify</servlet-name>
              <servlet-class>com.javastudy.oa.web.action.DeptModify</servlet-class>
          </servlet>
          <servlet-mapping>
              <servlet-name>modify</servlet-name>
              <url-pattern>/dept/modify</url-pattern>
          </servlet-mapping>
      

      展示修改页面

      package com.javastudy.oa.web.action;
      
      import com.javastudy.oa.utils.DBUtils;
      import jakarta.servlet.ServletException;
      import jakarta.servlet.http.HttpServlet;
      import jakarta.servlet.http.HttpServletRequest;
      import jakarta.servlet.http.HttpServletResponse;
      
      import java.io.IOException;
      import java.io.PrintWriter;
      import java.sql.Connection;
      import java.sql.PreparedStatement;
      import java.sql.ResultSet;
      import java.sql.SQLException;
      
      public class DeptEditServlet extends HttpServlet {
      
          @Override
          protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              // 获取应用的根路径
              String contextPath = req.getContextPath();
              resp.setContentType("text/html;charset=UTF-8");
              PrintWriter writer = resp.getWriter();
      
              writer.print("<!DOCTYPE html>");
              writer.print("<html lang='en'>");
              writer.print("<head>");
              writer.print("    <meta charset='UTF-8'/>");
              writer.print("    <meta http-equiv='X-UA-Compatible' content='IE=edge'/>");
              writer.print("    <meta name='viewport' content='width=device-width, initial-scale=1.0'/>");
              writer.print("    <title>修改部门</title>");
              writer.print("</head>");
              writer.print("<body>");
              writer.print("<h1 style='text-align: center'>修改部门</h1>");
              writer.print("<hr/>");
              writer.print("<form action='" + contextPath + "/dept/modify' align='center' method='post'>");
      
      
              // 获取部门编号
              String deptno = req.getParameter("deptno");
      
              // 连接数据库,根据部门编号查询部门信息
              Connection connection = null;
              PreparedStatement preparedStatement = null;
              ResultSet resultSet = null;
              try {
                  connection = DBUtils.getConnection();
                  String sql = "select dname,loc from dept where deptno = ?";
                  preparedStatement = connection.prepareStatement(sql);
                  preparedStatement.setString(1, deptno);
                  resultSet = preparedStatement.executeQuery();
                  if (resultSet.next()) {
                      String dname = resultSet.getString("dname");
                      String loc = resultSet.getString("loc");
      
                      // 动态内容
                      writer.print("部门编号<input type='text' name='deptno' value='" + deptno + "' readonly/><br/>");
                      writer.print("部门名称<input type='text' name='dname' value='" + dname + "'/><br/>");
                      writer.print("部门位置<input type='text' name='loc' value='" + loc + "'/><br/>");
                  }
              } catch (SQLException e) {
                  e.printStackTrace();
              } finally {
                  try {
                      DBUtils.close(connection, preparedStatement, resultSet);
                  } catch (SQLException e) {
                      e.printStackTrace();
                  }
              }
      
              writer.print("    <input type='submit' value='修改'/>");
              writer.print("</form>");
              writer.print("</body>");
              writer.print("</html>");
          }
      }
      

      真正的修改

      package com.javastudy.oa.web.action;
      
      import com.javastudy.oa.utils.DBUtils;
      import jakarta.servlet.ServletException;
      import jakarta.servlet.http.HttpServlet;
      import jakarta.servlet.http.HttpServletRequest;
      import jakarta.servlet.http.HttpServletResponse;
      
      import java.io.IOException;
      import java.sql.Connection;
      import java.sql.PreparedStatement;
      import java.sql.ResultSet;
      import java.sql.SQLException;
      
      public class DeptModify extends HttpServlet {
          @Override
          protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              req.setCharacterEncoding("UTF-8");
              String deptno = req.getParameter("deptno");
              String dname = req.getParameter("dname");
              String loc = req.getParameter("loc");
      
              // 连接数据库,根据部门编号查询部门信息
              Connection connection = null;
              PreparedStatement preparedStatement = null;
              ResultSet resultSet = null;
              int count = 0;
              try {
                  connection = DBUtils.getConnection();
                  String sql = "update dept set dname = ?,loc = ? where deptno = ?";
                  preparedStatement = connection.prepareStatement(sql);
                  preparedStatement.setString(1, dname);
                  preparedStatement.setString(2, loc);
                  preparedStatement.setString(3, deptno);
                  count = preparedStatement.executeUpdate();
              } catch (SQLException e) {
                  e.printStackTrace();
              } finally {
                  try {
                      DBUtils.close(connection, preparedStatement, resultSet);
                  } catch (SQLException e) {
                      e.printStackTrace();
                  }
              }
              if (count == 1) {
                  System.out.println("修改成功");
                  // 更新成功后跳转
                  req.getRequestDispatcher("/dept/list").forward(req, resp);
              } else {
                  System.out.println("修改失败");
                  // 更新失败后也跳转
                  req.getRequestDispatcher("/dept/list").forward(req, resp);
              }
          }
      }
      
    • 大功告成,但其实上面写的项目的跳转都应该用重定向,而不是转发

在一个web应用中应该如何完成资源的跳转

web应用中有两种方式完成资源的跳转

  1. 转发
  2. 重定向

转发和重定向有什么区别?

  1. 代码上的区别

    • 转发

      //获取请求转发器对象
      RequestDispatcher dispatcher = request.getRequestDispatcher("/dept/list");
      //调用请求转发器对象的forward方法完成转发
      dispatcher.forward(request, response);
      
      //可以合并成一句代码
      request.getRequestDispatcher("/dept/list").forward(req, resp);
      
      //转发的时候是一次请求,不管你转发了多少次,都是一次请求
      //AServlet转发到BServlet,再转发到CServlet,再转发到DServlet,不管转发了多少次,都在同一个request当中
      //这是因为调用Forward方法的时候,会将当前的request和response对象传给下一个Servlet,
      
    • 重定向

      response.sendRedirect(request.getContextPath() + "/B");// 项目名+xml里的地址
      // 重定向(重新定方向)
      // 重定向时的路径当中需要以项目名开始,或者说需要添加项目名,因为这个路径到时候由浏览器发送,当然需要项目名
      // response对象将这个路径:"/servlet06/B"响应给浏览器了
      // 浏览器又自发的向服务器发送了一次全新的请求:http://localhost:8080/servlet06/B
      // 所以浏览器一共发送了两次请求
      // 第一次请求:http://localhost:8080/servlet06/A
      // 第二次请求:http://localhost:8080/servlet06/B
      // 最终浏览器地址栏上显示的地址当然是最后那一次请求的地址,所以重定向会导致测览器地址栏上的地址发生改变
      
  2. 形式上的区别

    • 转发
      • 一次请求,请求后地址栏地址不变
    • 重定向
      • 两次请求,浏览器地址栏上显示的地址当然是最后那一次请求的地址,所以重定向会导致测览器地址栏上的地址发生改变

    本质区别

    转发:是由WEB服务器来控制的,A资源跳转到B资源,这个跳转动作是Tomcat服务器内部完成的 重定向:是这浏览器完成的,具体跳转到哪个资源,是浏览器说了算

转发和重定向如何选择?

如果在上一个Servlet当中向request域当中绑定了数据,希望从下一个Servlet当中把request域里面的数据取出来,使用转发机制 剩下所有的请求均使用重定向(重定向使用的多)

跳转的资源必须是一个servlet吗?不一定,只要是服务器里面的合法资源即可,包括servlet,jsp,html...

Servlet注解,简化开发

分析上面oa项目中的web.xml文件

  1. 现在只是一个简单的CRUD,没有复杂的业务逻辑,很简单的一丢丢功能,web.xml文件中就有如此多的配置信息,如果采用这种方式,对于一个大的项目来说,这样的话web.xml文件会非常庞大,有可能最终会达到几十兆
  2. 在web.xml文件中进行servlet信息的配置,显然开发效率比较低,每一个都需要配置一下
  3. 而且在web.xml文件中的配置是很少被修改的,所以这种配置信息能不能直接写到java类当中呢?可以的, Servlet3.0版本之后,推出了各种Servlet基于注解式开发,这样开发效率高,不需要编写大量的配置信息,直接在java类上使用注解进行标注,web.xml文件体积变小了
  4. 并不是说注解有了之后,web.xml文件就不需要了,有一些需要变化的信息还是要配置到web.xml文件中,一般都是注解+配置文件的开发模式,一些不会经常变化修改的配置建议使用注解,一些可能会被修改的建议写到配置文件中

注解里有哪些属性?

name 用来指定Servlet的名字,等同于<servlet-name>

urlPatterns 用来指定Servlet的映射路径,可以指定多个字符串<url-pattern>

value 这个value属性和urlPatterns属性一致,都是用来指定Servlet的映射路径的

loadOnStartup 用来指定在服务器启动阶段是否加载该Servlet,等同于<load-on-startup>

需要什么写什么,不是必须的

name = "s1"
urlPatterns = {"/a", "/b", "/c"}
loadOnStartup = 1
initParams = {@WebInitParam(name = "username", value = "root"), @WebInitParam(name = "password", value = "abc")}
...
注意:当注解的属性是一个数组,并且数组中只有一个元素,大括号可以省略
如果注解的属性名是value的话,属性名也是可以省略的,即@WebServlet("/bbb")
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

@WebServlet(name = "s1", urlPatterns = {"/a", "/b", "/c"})
public class AServlet extends HttpServlet {
    @Override()
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

    }
}

解决类爆炸

可以看到,上面的项目里一个单表的CRUD就有好多个servlet,如果一个复杂的业务系统,这种开发方式,显然会导致类爆炸(类的数量太多)

怎么解决上面问题?

可以使用模板方法设计模式,这样一个servlet就可以了

package com.javastudy.oa.web.action;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

// 模板类
@WebServlet({"/dept/list", "/dept/detail", "/dept/delete", "/dept/add", "/dept/edit", "/dept/modify"})
// @WebServlet("/dept/*")也行
public class DeptServlet extends HttpServlet {
    // 模板方法,重写service方法(并没有重写doGet或者doPost)

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取servlet Path
        String servletPath = req.getServletPath();
        if ("/dept/list".equals(servletPath)) {
            doListServlet(req, resp);
        } else if ("/dept/detail".equals(servletPath)) {
            doDetailServlet(req, resp);
        } else if ("/dept/delete".equals(servletPath)) {
            doDeleteServlet(req, resp);
        } else if ("/dept/add".equals(servletPath)) {
            doAddServlet(req, resp);
        } else if ("/dept/edit".equals(servletPath)) {
            doEditServlet(req, resp);
        } else if ("/dept/modify".equals(servletPath)) {
            doModifyServlet(req, resp);
        }
    }

    private void doModifyServlet(HttpServletRequest req, HttpServletResponse resp) {
        // 把之前的例子里的代码写到这里面就行了
    }

    private void doEditServlet(HttpServletRequest req, HttpServletResponse resp) {
    }

    private void doAddServlet(HttpServletRequest req, HttpServletResponse resp) {
    }

    private void doDeleteServlet(HttpServletRequest req, HttpServletResponse resp) {
    }

    private void doDetailServlet(HttpServletRequest req, HttpServletResponse resp) {
    }

    private void doListServlet(HttpServletRequest req, HttpServletResponse resp) {
    }
}

JSP

前面的servlet里写前端代码,难度大,程序的耦合度非常高,代码非常不美观,维护成本也很高,调个样式啥的还得跑去改后端java代码,重新编译部署

如果是你,你会怎么解决?

可以搞一个程序,让它帮忙打印之前我们servlet中写的那些输出语句如writer.print("<h1></h1>"),而我们程序员只需要专注写出前端代码就行,打印的事交给程序去做,而JSP就是这种机制

我的第一个jsp

在web应用的根下创建一个index.jsp文件即可

浏览器地址栏输入访问http://localhost:8080/jsp01/index.jsp ,实际上访问以上的这个index.jsp,底层执行的是:index_jsp.class这个java程序,这个index.jsp会被tomcat翻译生成index_jsp.java文件,然后tomcat服务器又会将index_jsp.java编译生成index_jsp.class文件,所以访问index.jsp,实际上执行index_jsp.class中的方法

jsp实际上就是一个servlet

  • index.jsp访问的时候,会自动翻译生成index_jsp.java,会自动编译生成index_jsp.class,那么index_jsp这就是一个类
  • index_jsp类继承HttpJspBase,而HttpJspBase类继承的是HttpServlet,所以index_jsp类就是一个Servlet类
  • jsp的生命周期和Servlet的生命周期完全相同,完全就是一个东西,没有任何区别
  • jsp和servlet一样,都是单例的(假单例)
  • jsp文件第一次访问是比较慢的,要把jsp文件翻译生成java源文件,java源文件要编译生成class字节码文件,然后通过class去创建servlet对象,然后调用servlet对象的init方法,最后调用servlet对象的service方法
  • 第二次就比较快了,为什么?因为第二次直接调用单例servlet对象的service方法即可

JSP是什么?

  • JSP是Java程序(JSP本质还是一个Servlet)
  • JSP是:JavaServer Pages的缩写,即基于Java语言实现的服务器端的页面
  • Servlet是JavaEE的13个子规范之一,那么JSP也是JavaEE的13个子规范之一
  • JSP是一套规范,所有的web容器/web服务器都是遵循这套规范的,都是按照这套规范进行的"翻译"
  • 每一个web容器/web服务器都会内置一个JSP翻译引擎

对JSP进行错误调试的时候,还是要直接打开JSP文件对应的java文件,检查java代码,开发JSP的最高境界是眼前是JSP代码,但是脑袋中呈现的是java代码

JSP的基础语法 在JSP文件中直接编写文字,都会自动被翻译到哪里? 翻译到servlet类的service方法的out.write("翻译到这里"),直接翻译到双引号里,被java当作普通字符串,打印输出到浏览器,但是浏览器会解析成页面,展现成效果

  1. 在jsp文件最前面写<%@ page contentType="text/html;charset=UTF-8" %>即可解决中文乱码问题

  2. jsp注释 <%-- --%>

  3. <% Java语句 %>括号内编写java语句便可被识别,在这个符号当中编写的被视为java程序,被翻译到Servlet类的service方法内部,这里你要细心点,要思考在<% %>这个符号里面写java代码的时候,要时时刻刻的记住你正在"方法体"当中写代码,方法体中可以写什么,不可以写什么心里要明

  4. <! >在这个符号当中编写的java程序会自动翻译到service方法之外,这个语法很少用,不建议使用,JSP就是servlet,servlet是单例的,多线程并发的环境下,这个静态变量和实例变量一旦有修改操作,必然会存在线程安全问题

  5. 怎么向浏览器输出一个变量?

    <%
        int i = 10;
        out.write(i);
    
    	out.write("asdfasdfasddf");//脱裤子放屁,不如直接在尖括号外面直接写
    %>    
    

    注意,以上的out是JSP的九大内置对象之一,可以直接拿来用

  6. 如果输出的内容中含有java代码,是个动态内容,这时可以用以下语法格式

    <%= %>,在=后面编写想输出的内容,<%= %>被翻译成了out.print()

    <%=100+200 %>,被翻译成out.print(100+200);注意200后面不要加分号;

JSP和Servlet有什么本质区别呢?

职责不同

  • servelt 收集数据,他的强项是逻辑处理,业务处理,连接数据库,获取/收集数据
  • jsp 展示数据

上面做的oa项目,就可以改造成jsp

  1. 将之前所有的页面原型html文件后缀全部修改为.jsp,防止中文乱码,在第一行加上<%@ page contentType="text/html;charset=UTF-8" %>,将所有的jsp文件拷贝到idea的web目录下,即应用的根

  2. 完成页面的正常跳转,修改超链接的请求路径,改成<%=request.getContextPath() %>/list.jsp,动态获取应用根路径

    <a href="<%=request.getContextPath() %>/list.jsp"></a>
    
  3. 在servlet中将数据库结果集中的数据按while循环收集数据封装到一个javabean对象中(创建Dept类,属性和表的字段一样),再将这些javabean对象塞进一个集合中,将这个集合用request.setAttribute("起个名",集合)放到request请求域中,再用request.getRequestDispatcher("/list.jsp").forward(request,response)转发给list.jsp

  4. 在jsp文件里,在本来需要循环展示的代码部分写这样的代码

    <%@page import="java.util.List, com.javastudy.oa.bean.Dept"%>  注意得导包
    
    
    <%
    	// 从request域中取出集合
    	List<Dept> deptList = (List<Dept>)request.getAttribute("刚才给集合起的名字")
        
    	// 遍历
    	for(Dept dept:deptList){
            // 后台输出
            // System.out.println(dept.getDname());
    <%        
        
    <tr>
        <td><%=dept.getDname()%></td>
    </tr>    
        
    <%
        }
    %>
    

只用 JSP 可不可以开发一个web应用?

当然可以使用 JSP 来完成所有的功能,因为JSP就是Servlet,在JSP的<%%>里面写的代码就是在service方法当中的,所以在<%%>当中完全可以编写,可以但没必要,应该各司其职,jsp是用来展示数据的,servlet负责收集数据,jsp中编写的java代码越少越好

jsp文件的拓展名必须是xxx.jsp吗?

是可以配置的,不是固定的,xxx.jsp文件对于小猫咪来说,只是一个普通的文本文件,web容器会将xxx.jsp文件最终生成java程序,最终调用的是java对象相关的方法,真正执行的时候,和jsp文件就没有关系了

什么是javabean?

实际上javabean你可以理解为符合某种规范的java类,通常放在bean包下,用来封装零散的数据

  1. 有无参数构造方法
  2. 属性私有化
  3. 对外提供公开的set和get方法
  4. 实现java.io.Serializable接口
  5. 重写toString
  6. 重写hashCode+equals
  7. ...

符合上面的规范,有更强的通用性

引出问题,当前的项目任何用户都可以访问,只想让合法用户访问该怎么办?

可以加一个登录功能:

  1. 先写一个登录页面

    登录页面上应该有一个登录的表单,有用户名和密码输入的框,用户点击登录,提交表单,提交用户名和密码,form是post方式提交

  2. 给数据库中添加一个用户表,里面存储用户名和密码,密码实际上不以明文存储

  3. 写一个servlet处理登录请求

    成功跳转到部门列表页面

    失败跳转到失败页面(得写一个)

  4. 密码正确能跳转到相应的页面,但是要是在地址栏直接输入正确的地址,也能访问登录成功后的页面,怎么解决呢?这时就可以引出session了

JSP指令

指令的作用:指导翻译引擎如何工作

<%@指令名 属性名=属性值 属性名=属性值 属性名=属性值...%>

指令包括那些?

  1. include指令 包含指令,在jsp中完成静态包含,很少用了

  2. taglib指令 引入标签库指令,这个到 JSTL 标签库时再学习,先不管

  3. page指令

    1. <%@page session="true或false" %>true表示启用JSP的内置对象session,表示一定启动session对象,没有session会创建,没设置默认为true

    2. <%@page contentType="text/json" pageEncoding="UTF-8" %>设置响应的内容类型

    3. <%@page improt="java.util.List, java.util.Date" %>导包

    4. <%@page errorPage="/error.jsp" %>服务器错误后跳转到指定页面,不会再显示错误代码了

      但是这样设置在发生错误后后台也不会报错了,可以启用九大内置对象之一的exception

      <%@page isErrorPage="true" %><% exception.printStackTrace(); %>

    5. <%@page %>

JSP的九大内置对象

  1. pageContext 页面作用域
  2. request 请求作用域
  3. session 会话作用域
  4. application 应用作用域

pageContext<request<session<application

且以上四个作用域都有setAttribute,getAttribute,removeAttribute方法,尽量使用小的域

  1. exception

异常

  1. config

配置

  1. page

this,当前的servlet对象,没啥用

  1. out
  2. response

out负责输出

response负责响应

EL表达式

实际上是jsp的语法,表达式语言(Expression Language)

有啥用呢?

  • EL表达可以代替JSP中的Java代码,让JSP文件里的程序看起来更加整洁美观
  • JSP中夹杂着如<% java代码 %>等,导致特别乱
  • EL表达式归属于JSP,是JSP语法的一部分

EL表达式出现在JSP中主要是

  • 从某个作用域中取数据,然后将其转换成字符串,然后将其输出到浏览器,这就是EL表达式的功效,三大功效:
    • 从某个域中取数据(只负责取数据!)
    • 将取出的数据转换成字符串,如果取出一个对象,自动调用toString方法
    • 将字符串输出到浏览器

EL表达式很好用,基本的语法格式:

  • ${表达式}

  • ${userObj}   // 等同于<%=request.getAttribute("userObj")%>,user是个对象
    直接输出对象,则底层调用了toString方法
    能写上面的${userObj},说明之前一定有这样的代码:	域.setAttribute("userObj",对象)
    

${ }内直接可以写对象 . 属性的方式,输出对象的属性值,实际上底层在调用getXXX方法,前提得有getXXX方法

注意,在EL表达式内不能加双引号,会当作普通字符串输出

没有指定取出范围的情况下,EL表达式优先从小范围的域中取数据

假如现在四个域中都有同名的属性,怎么取出指定的?

session.setAttrubute("data","session");
request.setAttrubute("data","request");
pageContext.setAttrubute("data","pageContext");
application.setAttrubute("data","application");

可以前面加个限定,就能按指定域范围取数据了
${pageScope.data}
${requestScope.data}
${sessionScope.data}
${applicationScope.data}

在实际开发中,因为在不同域中的存数据的时候,name是不同的,所以xxxScope一般不写

${obj.name}也可以这样写${obj["name"]}

假如EL表达式没取到数据,不会在页面上展示个null,比<%=request.getAttribute("xxx") %>更友好

使用EL表达式怎么从Map集合中取数据? ${map.key}

使用EL表达式怎么从数组和List集合中取数据? ${arr[1]}

page指令中,有一个属性,可以忽略EL表达式

<%@page isELIgnored="true" %>  true表示忽略EL表达式,false表示不忽略

\${user}  表示忽略单个EL表达式(加反斜杠)

使用EL表达式获取应用根路径${pageContext.request.contextPath}

使用EL表达式获取用户传过来的参数${param.age}输出age对应的值

使用EL表达式获取用户传过来的同名参数的多个值${paramValues.aihao[1]}

使用EL表达式获取xml中配置的默认信息${initParam.xxx}

EL表达式中的运算符

+	-	*	/

==	!=	>	>=	<	<=	eq	

!	&&	||	not	and	or

? :

[]	和	.

empty

加号只能求和,不能拼接字符串,==和eq和!=都调用了equals方法
!not都是取反,empty判断是否为空,是空为true

总结

EL就是用来从域中取数据然后输出

Session

可以看出,上面的登录功能就是个摆设,输入正确的地址照样能访问登录后才能看的内容,在不登陆的状态下也能访问

这时就需要一个会话机制Session,B/S架构都有session机制,这种会话机制实际上是种规范

什么是会话?

用户打开浏览器,进行一系列操作,最终将浏览器关闭,这个过程就叫一次会话,会话在服务器端有一个对应的java对象,这个java对象叫session,最主要的作用是保存会话状态(这是一种用户登录成功的状态)

什么是请求?

用户在浏览器点击一下,然后到页面停下来,可以粗略认为是一次请求,对应的也在服务器端有一个java对象request,一次session会话对应N次请求

为什么需要session对象保存会话状态呢?

  • 因为HTTP协议是一种无状态协议

  • 什么是无状态?请求的时候,B和S是连接的,但是请求结束之后,连接就断了,为什么要这么做,HTTP协议为什么要设计成这样?因为这样的无状态协议,可以降低服务器的压力,请求的瞬间是连接的,请求结束之后,连接断开,这样服务器压力小

  • 只要B和S断开了,那么关闭览器这个动作,服务器知道吗,不知道,服务器是不知道浏览器关闭的

  • 张三打开一个浏览器A,李四打开一个浏览器B,访问服务器之后,在服务器端会生成 张三专属的session对象 李四专属的session对象

  • 有些网站的安全退出,其实就是主动销毁session对象,取消正在登录的状态

  • 为什么不使用request对象保存会话状态?为什么不使用ServletContext对象保存会话状态?

    request是一次请求一个对象,域太小,ServletContext对象是服务器启动的时候创建,服务器关闭的时候销毁,这个ServletContext对象只有一个,ServletContext对象的域太大,尽量使用小的域

  • request请求域(HttpServletRequest)请求级别的

    session会话域(HttpSession)用户级别的

    application应用域(ServletContext)项目级别的,所有用户共享

    他们三个域都有setAttribute,getAttribute,removeAttribute方法

  • request < session < application

package com.study;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import java.io.IOException;

@WebServlet("/test/session")
public class TestSessionServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // request和session都是服务器中的java对象
        // 先看request对象,一次请求代表一个request对象,两次代表两个
        // session对象代表一次会话,

        // 从服务器中获取当前session对象
        // 如果没有获取到任何session对象,则新建
        HttpSession session = request.getSession();
        
        // 从服务器中获取session对象
		// 如果没有获取到任何session对象,则不会新建,返回null
		// HttpSession session1 = request.getSession(false);

        // 向会话域中绑定数据
        session.setAttribute();
        // 从会话域中取数据
        session.getAttribute();
    }
}

HttpSession session = request.getSession();这句话很神奇,为什么张三在浏览器A访问,是一个Session对象,而李四在浏览器B访问却是另一个session对象呢?为什么张三不会把李四的session对象拿到呢?

  • 实际上在Tomcat服务器上有一个session列表,有key和value,key里面存session的编号,value里存session的对象,用户发送第一次请求的时候,服务器会创建一个新的session对象,同时给session对象生成一个id,然后将id发送给浏览器,浏览器将session的id保存在浏览器的缓存中

  • 用户发送第二次请求的时候,会自动将浏览器缓存中的session id发给服务器,服务器会将接收到的id在session列表中查找对应的session对象,只要关闭浏览器,缓存一清除再次打开就没有session id了,所以会新建一个会话(session对象)

  • 到底什么是一次会话?其实就是session对象从创建到死掉,这就是一次会话

  • 假如张三现在关闭了浏览器,服务器是不知道的,因为http协议是无状态协议,但是session有个超时机制,只要超过特定的时间未向服务器发送数据,那么服务器端的session对象就会被销毁

        <!--设置session的超时时长为10分钟,默认是30-->
        <session-config>
            <session-timeout>10</session-timeout>
        </session-config>
    
    	<!--具体设置多久看系统,比如网银系统3分钟就行-->
    
  • session对象什么时候销毁?1.超时销毁2.手动销毁(安全退出,这时服务器立刻销毁session对象)'

  • 浏览器将session id 以cookie的形式保存在浏览器当中的(JSESSIONID=XXXXXXXXXXXXX),注意是放在内存中的,不是在硬盘里,只要浏览器退出就没了

禁用cookie是什么意思?

不是说是服务器不发了,而是浏览器发过来浏览器不要了,每一次请求都会收到新的session对象,那禁用后session机制还能实现吗?可以,通过url重写机制解决,在地址栏url后面加一个分号;再跟上jsessionid=xxxxxxxxxxx,但是会提高开发者的成本,在编写任何请求路径时后面都要加一个sessionid,特别麻烦,所以大部分的网站都是这样设计的:你要是禁用cookie,你就别用了

我们掌握session后,如何改进上面案例的登录问题?

登陆成功后可以将用户信息存到session对象中,session中有信息则代表登陆成功,没有信息则跳转到登录页面

if (success){
    HttpSession session = request.getSession();
    session.setAttribute("username",username);
}

用户登录后才能执行对应的业务操作,否则回到登录页

// 只是获取当前session,获取不到返回null
HttpSession session = request.getSession(false);
     
if (session != null && session.getAttribute("username") != null) {
    String servletPath = request.getServletPath();
    if ("/dept/list".equals(servletPath)) {
        doListServlet(request, response);
    } else if ("/dept/detail".equals(servletPath)) {
        doDetailServlet(request, response);
    } else if ("/dept/delete".equals(servletPath)) {
        doDeleteServlet(request, response);
    }
} else {
    response.sendRedirect("/oa/index.html");
}

注意,在jsp中写<%@page session="false" %>可以禁用jsp自带的session对象

安全退出,手动销毁session对象

// 获取session对象,销毁它
HttpSession session = request.getSession(false);
if (session != null) {
    // 手动销毁session对象
    session.invalidate();
}

cookie

session的实现原理中,每一个session对象都会关联一个sessionid,例如

JSESSIONID=adfadsfasdfsdfadf

以上这个键值对数据其实就是cookie对象

对于session管理的cookie来说,这个cookie是被保存在浏览器的"运行内存"中,只要浏览器不关闭,用户再次发送请求时,将自动将运行内存中的cookie发送给服务器

问题来了,cookie怎样生成?存在哪里?有啥用?浏览器什么时候,发送什么cookie给服务器?

  • cookie最终保存在浏览器上
    • 可以保存在运行内存中,只要关闭浏览器cookie就消失了
    • 也可以保存在硬盘文件中,永久保存
  • cookie和session机制其实都是为了保存会话的状态
  • cookie是将会活的状态保存在浏览器客户端上(cookie数据存储在浏览器客户端上)
  • session是将会话的状态保存在服务器端上(session对象是存储在服务器上)
  • 为什么要有cookie和session机制呢?因为HTTP协议是无状态无连接协议
  • 在HTTP协议中是这样规定的:当浏览器发送请求的时候,会自动携带该path下的cookie数据给服务器,访问不同的url带该url对应的cookie

cookie的经典案例:

  • 比如京东商城在没登陆的状态下可以加购物车,关闭浏览器发现商品还在,是因为其将商品编号信息放到了cookie中,保存到了硬盘中,即使关闭浏览器,下次打开还在
  • 比如十天免登录功能:用户输入正确的用户名和密码,并且同时选择十天内免登录,登录成功后,浏览器客户端会保存一个cookie,这个cookie中保存了用户名和密码等信息,这个cookie是保存在硬盘文件当中的,十天有效,在十天内用户再次访问126的时候,浏览器自动提交126的关联的cookie给服务器,服务器接收到cookie之后,获取用户名和密码,验证,通过之后,自动登录成功,怎样让cookie失效?改密码,或10填自动失效,或手动删除cookie

实际上cookie机制和session机制其实都不属于Java中的机制,实际上cookie机制和session机制都是HTTP协议的一部分,php开发中也有cookie和session机制,要是你是做web开发,不管是什么编程语言,cookie和session机制都是需要的

HTTP协议中规定:任何一个cookie都是name和value组成,都是字符串类型

在Java的servlet中,对cookie提供了那些支持呢?

提供了一个类专门表示cookie数据,但是服务器要造cookie对象干啥呢?是因为java响应类的addCookie(cookie)方法可以携带cookie发送给浏览器,让浏览器保存

// 服务器生成cookie,发给浏览器,浏览器接受到cookie后保存到客户端上
Cookie cookie = new Cookie("id","123");
response.addCookie(cookie);//发送

可以设置cookie的有效时间,以秒为单位cookie.setMaxAge(60 * 60);// 设置一小时后失效

没有设置有效时间默认是保存在内存中,只要关闭浏览器就会消失

只要设置cookie的有效时间>0,一定是存在硬盘中

设置cookie的有效期为0,表示该cookie被除,主要应用在,使用这种方式除浏览器上的同名cookie,有效期小于0表示不会存储该cookie,表示不会被存储到硬盘中,会放在浏览器运行内存中,和不调用setMaxAge是一个效果

关于cookie的path,cookie关联的路径

假设现在发送的请求路径是"http://localhost:8080/servlet13/cookie/generate"生成的cookie,如果cookie没有设置path,默认的path是什么?默认的path是:http://localhost:8080/servlet13/cookie以及它的子路径,也就是说,以后只要浏览器的请求路径是http://localhost:8080/servlet13/cookie这个路径以及这个路径下的子路径,cookie都会被发送到服务器

手动设置cookie的path

cookie.setPath("/oa");// 表示oa项目里所有的请求路径,都会将这个cookie发给服务器

现在浏览器将cookie发过来了,服务器该怎样接受cookie?

当然是在request里的方法

// 得到浏览器发过来的所有cookie,要是没有cookie的时候,返回的是null,并不是空数组
Cookie[] cookies = request.getCookies();
if (cookies != null) {
    for (Cookie cookie : cookies) {
        // 获取cookie的name
        String name = cookie.getName();
        // 获取cookie的value
        String value = cookie.getValue();
    }
}

JSTL标签库

什么是JSTL标签库?

  • Java Standard Tag Lib (Java标准的标签库)
  • JSTL标签库通常和EL表达式一起使用,目的是让JSP中的Java代码消失
  • 标签是写在JSP当中的,但实际上还是要执行对应的java程序(java程序在jar包中)

使用步骤

  1. 引入JSTL标签库对应的jar包

    tomcat10之后引入的是

    jakarta.servlet.jsp.jstl-2.0.0.jar taglibs-standard-impl-1.2.5.jar taglibs-standard-spec-1.2.5.jar(自己找)

    在IDEA中在WEB-INF下的lib目录中放入jar包,然后右键添加到项目,和mysql驱动一样

  2. 在jsp文件中引入JSTL标签库

    JSTL提供了很多种标签,你要引入哪个标签?重点掌握核心标签库

    <%@taglib prefix="xxxx" uri="http://java.sun.com.jsp.jstl.core" %>
    这是核心标签库,prefix="xxxx"里通常写"c"
    
  3. 在需要使用的地方使用即可,表面使用的是标签,底层实际上还是java程序

    <%@taglib prefix="c" uri="http://java.sun.com.jsp.jstl.core" %>
    
    <c:forEach items="${stuList}" var="s">
        id:${s.id},name:${s.name}
    </c:forEach>
    
  4. 有哪些常用标签呢?

    <c:if test="">
        test内可以写EL表达式,Boolean类型,为真显示里面内容
        类似vue中的v-if
    </c:if>
    
    <c:forEach var="i" begin="1" end="10" step="1">
        ${i}    循环输出1-10,var指定循环中的变量
    </c:forEach>
    
    <c:choose>  if
        <c:when test="${param.age<18}">
        	未成年
        </c:when>
        <c:when test="${param.age>=18}">
        	成年
        </c:when>
        <c:otherwise>
            输入有误
        </c:otherwise>
    </c:choose>
    

过滤器

多个处理不同业务的servlet中,一些公共的代码,如设置请求字符集,响应字符集,cookie判断用户是否登录等

之前的代码中没有复用这些代码,怎么解决?

可以使用servlet规范中的Filter过滤器

  1. 可以在servlet执行之前过滤,也可以执行之后过滤

  2. 过滤器也是java程序,也是类

  3. 过滤器也是通过不同的请求路径通过对应的过滤器

  4. 一次请求可能通过的过滤器有多个,有解决乱码的过滤器,有安全控制的过滤器,有判断登录的过滤器

  5. 一般在过滤器中写公共代码

  6. 注意,Servlet对象在默认情况下,服务器启动时不会新建对象

    Filter对象在默认情况下,在服务器启动时会新建对象

    Servlet是单例的,Filter也是单例的(单实例,只创建一个)

如何写一个过滤器呢?

  1. 编写一个java类实现一个接口:jarkata.servlet.Filter,并实现这个接口的所有方法

    • init 在Filter对象第一次被创建后执行,只调用一次
    • doFilter 只要用户发送一次请求,执行一次,发送N次,执行N次
    • destroy 在Filter对象被释放/销毁前执行,只调用一次
    package com.study;
    
    import jakarta.servlet.*;
    
    import java.io.IOException;
    
    public class MyFilter implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            Filter.super.init(filterConfig);
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    
        }
    
        @Override
        public void destroy() {
            Filter.super.destroy();
        }
    }
    
  2. 在web.xml文件中对Filter进行配置,和servlet配置很像

        <filter>
            <filter-name>filter1</filter-name>
            <filter-class>com.study.MyFilter</filter-class>
        </filter>
        <filter-mapping>
            <filter-name>filter1</filter-name>
            <url-pattern>/abc</url-pattern>
            <url-pattern>/def</url-pattern>
        </filter-mapping>
    
    既可以在xml文件里配置成这样
    也可以在Filter类之前加注解  @WebFilter({"/abc", "/def"})  也行
    
  3. 假如现在有个AServlet,他的路径是/a,如下所示

    package com.study;
    
    import jakarta.servlet.ServletException;
    import jakarta.servlet.annotation.WebServlet;
    import jakarta.servlet.http.HttpServlet;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    
    import java.io.IOException;
    
    @WebServlet("/a")
    public class AServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            System.out.println("A Servlet 的doGet方法执行了");
        }
    }
    

    有一个过滤器MyFilter,上面有注解@WebFilter("/a")

    package com.study;
    
    import jakarta.servlet.*;
    import jakarta.servlet.annotation.WebFilter;
    
    import java.io.IOException;
    
    @WebFilter("/a")
    public class MyFilter implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            Filter.super.init(filterConfig);
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            System.out.println("doFilter被执行了");
        }
    
        @Override
        public void destroy() {
            Filter.super.destroy();
        }
    }
    

    这时你访问项目名/a,发现跳转不到AServlet了,此时MyFilter中的doFilter方法执行了,这就被拦截了

    想让向下走,执行AServlet,就得在doFilter方法里写

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("doFilter被执行了");
        filterChain.doFilter(servletRequest,servletResponse);
    }
    

    注意,@WebFilter("/a")里可以写/xx/*或者*.xx或者/*表示匹配模糊或者匹配所有

目标Servlet是否执行,取决于两个条件

  • 第一,在过滤器当中是否编写了chain.doFilter(request,response)代码
  • 第二,用户发送的请求路径是否和Servlet的请求路径一致

chain.doFilter(request,response)这行代码的作用

  • 执行下一个过滤器,如果下面没有过滤器了,执行最终的Servlet
  • 注意Filter的优先级,天生的就比Servlet优先级高,假如/a.do对应一个Filter,也对应一个Servlet,那么一定是先执行Filter,然后再执行Servlet

Filter的执行顺序?

  • 在web.xml文件中多个filter依靠filter-mapping标签的顺序依次执行
  • 在@WebFilter({"/b","/a"})配置的按照过滤器类的名字大小依次进行,比如FilterA和FilterB先执行FilterA,Filter1和Filter2先执行Filter1

Filter的生命周期

和Servlet的生命周期一致,唯一区别是Filter在服务器启动时会实例化,而Servlet不会

Filter的设计模式:责任链设计模式

Filter的最大的优点:在程序编译阶段不会确定调用顺序,因为Filter的调用顺序是配置到web.xml文件中的,只要修改web.xml配置文件中filter-mapping的顺序就可以调整Filter的执行顺序,显然Filter的执行顺序是在程序运行阶段动态组合的,那么这种设计模式被称为责任链设计模式,责任链设计模式最大的核心思想:在程序运行阶段,动态的组合程序的调用顺序

监听器

监听器时Servlet规范中的一员,就像Filter一样,也是servlet规范中的一员

在Servlet中,所有的监听器接口都是以"Listener"结尾

监听器有什么用?

  • 监听器实际上是Servlet规范留给我们javaweb程序员的特殊时机
  • 特殊的时刻如果想执行这段代码,你需要想到使用对应的监听器

Servlet规范中提供了哪些监听器?

  • jakarta.servlet包下

    • ServletContextListener
    • ServletContextAttributeListener
    • ServletRequestListener
    • ServletRequestAttributeListener
  • jakarta.servlet.http包下

    • HttpSessionListener

    • HttpSessionAttributeListener

      • 该监听器需要使用@WebListener注解进行标注
      • 该监听器监听的是session域中数据的变化,只要数据变化,就会执行相应的方法
      • 主要监听点在session域对象上
    • HttpSessionBindingListener

      • 该监听器不需要使用@WebListener进行标注

      • 假设User类实现了该监听器,那么User类对象被放入session域中会触发bind事件

        User类对象被从session域中删除的时候,触发unbind事件

      • 假设Customer类没有实现该监听器,那么Customer类对象被放入session域或者删除时不会触发bind和unbind事件

    • HttpSessionldListener

      • session的id发生改变的时候,监听器中的唯一个方法就会被调用
    • HttpSessionActivationListener

      • 监听session对象的钝化和活化的 钝化:session对象从内存存储到硬盘文件 活化:从硬盘文件把session恢复到内存

实现监听器的步骤

  1. 编写一个类,实现ServletContextListener接口,并实现里面的方法

    package com.study;
    
    import jakarta.servlet.ServletContextEvent;
    import jakarta.servlet.ServletContextListener;
    
    public class MyListener implements ServletContextListener {
        @Override
        public void contextInitialized(ServletContextEvent sce) {
            //想在这个特殊的时刻写代码,你写就是了,它会被服务器自动调用
        }
    
        @Override
        public void contextDestroyed(ServletContextEvent sce) {
            //想在这个特殊的时刻写代码,你写就是了,它会被服务器自动调用
        }
    }
    
  2. 在web.xml文件中对MyListener进行配置

    <listener>
        <listener-class>com.study.MyListener</listener-class>
    </listener>
    

    同过滤器一样,可以使用注解的方式配置@WebListener

    @WebListener
    public class MyListener implements ServletContextListener {
        @Override
        public void contextInitialized(ServletContextEvent sce) {
            //这个方法是在ServletContext对象被创建的时候调用
            //服务器启动时间点,想在这个时候执行一段代码,写就行了
        }
    
        @Override
        public void contextDestroyed(ServletContextEvent sce) {
            //这个方法是在ServletContext对象被销毁的时候调用
            //服务器关闭时间点,想在这个时候执行一段代码,写就行了
        }
    }
    
  3. 注意,所有监听器的方法都是服务器调用,不用javaweb程序员调用,什么时候调用呢?

    是发生某个特殊事件后被服务器调用

    如ServletContextListener监听器主要监听的是ServletContext对象的状态

思考一个业务场景:记录该网站实时的在线用户的个数

我们可以通过服务器端有没有分配session对象判断,因为一个session代表了一个用户,有一个session就代表有一个用户,如果你采用这种逻辑去实现的话session有多少个,在线用户就有多少个,这种方式的话HttpSessionListenert够用了,session对象只要新建,则count++,然后将count存储到ServletContext域当中,在页面展示在线人数即可

那要是想统计登录的在线人数的数量,该怎么办?

只要登录,说明肯定有session.setAttribute("user",userObj),只要退出,session.removeAttribute("user")肯定要执行或者session超时被销毁,这时候就要用HttpSessionBindingListener监听器了,会检测有没有往session里存过特定类型的对象,有添加过userObj才会触发监听器