Servlet 深入复习

初学 Java Web 开发,请远离各种框架,从 Servlet 开发

1、JDBC的诞生

1.1 JDBC的诞生

JDBC是 Java 连接数据库的标准,是连接数据库的抽象层,由Java 编写的一组类和接口组成,接口的实现由各个数据库厂商来完成

这个标准就是指Connection、Statement、ResultSet接口,分别用于连接数据库,执行SQL语句、返回结果。从Connetion 可以创建Statement,Statement执行查询后可以得到ResultSet。这些接口的具体实现都是由各个数据库厂商来完成。

如果直接new 一个 Connection 实现类对象的话,当Connection实现类版本更迭,名称修改了就无法使用了。因此我们提倡面向接口编程而不是向实现编程。由电脑硬件驱动启发,创建一个抽象层 Driver,通过Driver来获取数据库连接Connection但是Drive的实现类也不能直接new,所以通过反射,在运行时动态装载。例如mysql 的Driver是这样的:Class.forName(“com.mysql.jdbc.Driver”);Oracle 的是这样的:Class.forName(“oracle.jdbc.driver.OracleDriver”);只要Driver不变,其具体的实现像Connection, Statement, ResultSet想怎么改就怎么改。

还有一个问题就是 一个程序可能访问不同的数据库,可能有多个不同的Driver都被动态加载,如何管理?

通过DriverManager类,将mysql的Driver、Oracle的Driver在类的初始化时,注册到DriverManager中,这样就可以通过DriverManager来管理。

注意: 关键点是static 的代码块, 在一个类被装载后就会执行。

//简化版DriverManager
public class DriverManager{
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
 
    public static Connection getConnection(String url,
        String user, String password) throws SQLException {
        java.util.Properties info = new java.util.Properties();
 
        if (user != null) {
            info.put("user", user);
        }
        if (password != null) {
            info.put("password", password);
        }
        for(DriverInfo aDriver : registeredDrivers) {
           Connection conn =  aDriver.getConnection(url,info);
            if(conn!=null){
                return conn;
            }
        }
    
       throw new RuntimeException("cant not create a connection")
    }
    //把实现类的Driver注册进一个ArrayList里
    public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {
        if(driver != null) {
            registeredDrivers.addIfAbsent(new DriverInfo(driver));
        }
 
    }

DriverManager会提供一个getConnection的方法,用于建立数据库Connection,只需要提供参数url、user、password。DriverManager会把这些信息传递给每个Driver , 让每个Driver去创建Connection

《码农翻身》之DBC的诞生

1.2 连接数据库的步骤

  1. 加载注册驱动
  2. 获取连接对象
  3. 创建语句对象
  4. 执行Sql语句
  5. 释放资源

1.3 数据库连接池

  1. 什么是数据库连接池

是程序启动时创建足够多个数据库连接,将这些连接组成一个连接池,由程序动态的对池中的连接进行申请、使用、释放。

连接是一种状态,即彼此感受到对方的存在,建立连接是维护一种状态,维护状态就需要一定的数据结构,数据库连接池就可以将建立的连接用链表存储起来。

  1. 什么要使用数据库连接池?
  • 数据库的每一次连接都要经历三次握手,mysql认证,执行sql,四次挥手,这是十分耗费资源和时间的;
  • 并且每一次的连接都没有很好的重复利用,如果并发量大的话,将会还会占用更多的系统资源;
  • 我们何不在程序初始化时,集中创建多个数据库连接,集中分配、管理、关闭,这样即可加快速度,充分利用资源。
  1. 连接池的分类

在Java中,连接池使用javax.sql.Datasource接口来表示连接池,Java中没有提供连接池的实现。

注意:DataSource仅仅只是一个接口,由各大厂商来实现。

常用的DataSource的实现:

  • DBCP:Spring推荐的、
  • C3PO: Hibernate推荐的
  • Druid:阿里巴巴提供,号称世界上性能最好的连接池
  • Datasource(数据源)和连接池(connection Pool)是同一个

2、Tomcat(web服务器)

2.1 介绍

Tomcat 服务器是一个免费的开放源代码的Web 应用服务器,属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP 程序的首选。对于一个初学者来说它是最佳的选择。Tomcat 实际上运行JSP 页面和Servlet。

2.2 Tomcat目录结构介绍

  • bin:主要存放tomcat命令,分为两大类
    • 一类是以.sh结尾的(Linux命令)
    • 另一类是以.bat结尾的(Windows命令)
    • startup 用来启动tomcat
    • shutdown 用来关闭tomcat
  • conf:主要存放tomcat配置文件
    • server.xml 可以设置端口号、设置域名或IP、默认加载的项目、请求编码.
    • web.xml
  • lib:主要存放tomcat运行依赖的jar包
  • logs:存放tomcat运行过程产生的日志文件(清空不会产生影响)
    • 在windows环境中,控制台的输出日志在catalina.xxxx-xx-xx.log文件中
    • 在linux环境中,控制台的输出日志在catalina.out文件中
  • temp:存放tomcat运行过程中产生的临时文件
  • webapps:用来存放应用程序,当tomcat启动时会去加载webapps目录下的应用程序。可以以文件夹、war包、jar包的形式发布应用
  • work:用来存放tomcat在运行时的编译后文件,例如JSP编译后的文件。清空work目录,然后重启tomcat,可以达到清除缓存的作用

3、servlet

3.1 什么是Servlet?

  • Servlet是基于Java技术的web组件,是一个接口、一个类;运行在服务器端,由Servlet容器所管理。
  • Servlet还是一个动态网页规范;编写一个Servlet,实际上就是按照Servlet规范编写一个Java类。
  • Servlet 功能就是用于浏览器和服务端进行动态交互,生成动态网页。

管理运行 Servet/JSP 的容器称为Web容器。Servlet 容器=JSP容器=Web容器 ,Servlet不能独立运行,必须部署到Servlet容器中,由容器实例化和调用 Servlet方法,生命周期由容器所管理。

请求处理过程

  1. 用户通过URL请求访问Servlet,Web服务器接收到请求,并不是直接交给Servlet,而是交给Servet容器。
  2. Servlet容器实例化Servlet,调用Servlet的特定方法对请求进行处理,并产生一个响应。
  3. 这个响应由Web 服务器包装并返回给Web服务器,以HTTP响应的形式发给Web浏览器。
image-20210411171140610

注意:Servlet必须有公共的无参数构造器

原因:底层使用的是反射的方式创建对象,用到了Class对象.newInstance()方法;该方法要求类有公共无参数构造器

3.2 Servlet API 组成

Java Servlet API由两个软件包组成:一个是对应HTTP的软件包,另一个是不对应HTTP的通用的软件包

  • 软件包:javax.servlet

    • 所包含的接口:
      • Servlet
        ServletRequest
        ServletResponse
        RequestDispatcher
        ServletConfig
        ServletContext
        SingleThreadModel
        Filter
        FilterConfig
        FilterChain
        ServletRequestListener
        ServletRequestAttributeListener
        ServletContextListener
        ServletContextAttributeListener
    • 所包含的类:
      • GenericServlet
        ServletInputStream
        ServletOutputStream
        ServletRequestWrapper
        ServletResponseWrapper
        ServletRequestEvent
        ServletContextEvent
        ServletRequestAttributeEvent
        ServletContextAttributeEvent
        ServletException
        UnavailableException
  • 软件包:javax.servlet.http

    • 所包含的接口:
      • HttpServletRequest
        HttpServletResponse
        HttpSession
        HttpSessionListener
        HttpSessionAttributeListener
        HttpSessionBindingListener
        HttpSessionActivationListener
    • 所包含的类:
      • HttpServlet
        Cookie
        HttpServletRequestWrapper
        HttpServletResponseWrapper
        HttpSessionEvent
        HttpSessionBindingEvent

Servlet API主要的类和接口:

image-20210411192948388

1、Servlet 接口

1、init

void init(ServletConfig config) ;
  • 对于每个 Servlet 实例,init()方法只能被调用一次。

  • 在 Servlet 实例化后,Servlet 容器会调用 init() 方法来初始化该对象。

  • init() 方法上ServletConfig类型的参数,可以用来获取 web.xml 中的配置的初始化参数信息。

  • 另外,还可以通过 ServletConfig对象获取描述Servlet 运行环境的 ServletContext 对象,使用该对象,Servlet 可以和它的Servlet容器进行通信。

2、service

void service(ServletRequest req, ServletResponse res) ;
  • 对于每个请求,service()方法都会执行一次。
  • 容器调用 service() 方法来处理客户端请求,在调用该方式时,要确保init()方法完成。
  • 容器会构造一个表示客户端请求信息的请求对象(类型为ServletRequest)和一个用于对客户端进行响应的响应对象(类型为ServletResponse)作为参数传递给service()方法。

3、destory

void destroy() ;

由servlet容器调用,表示servlet正在退出服务。在Servlet 容器调用 destory()方法前,如果还有其他的线程正在 service()方法中执行,容器会等等这些线程执行完毕或等待服务器设定的超时值到达。

4、getServletConfig

ServletConfig getServletConfig() ;
  • 该方法返回容器调用init()方式时传递给Servlet 对象的ServletConfig对象,ServletConfig对象包含了Servlet的初始化参数。
  • 作为一个Servlet的开发者,你应该通过init()方法存储ServletConfig对象以便这个方法能返回这个对象。为了你的便利,GenericServlet在执行这个接口时,已经这样做了

5、getServletInfo

String getServletInfo() ;
  • 返回Servlet类的信息,作者,版权等信息。几乎不用。

2、ServletConfig 接口

在javax.servlet 包中,定义了ServletConfig接口。Servlet容器使用ServletConfig对象在Servlet初始化期间向它传递配置信息。一个Servlet只有一个ServletConfig对象

1、getInitParameter

public String getInitParameter(String name);
  • 用于获取Servlet配置的初始化参数,该参数是程序员在开发的时候配置在web.xml中的

    • <!--web.xml-->
        <servlet>
        	<servlet-name>springMVC</servlet-name>
        	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        	<!--告诉spring去哪里寻找配置文件  -->
        	<init-param>
        		<param-name>contextConfigLocation</param-name>
        		<param-value>classpath:applicationContext_website.xml</param-value>
        	</init-param>
        	<load-on-startup>1</load-on-startup>
        </servlet>
      

2、getInitParameterNames

public Enumeration getInitParameterNames();
  • 返回所有初始化参数的名字的集合。如果Servlet没有初始化参数,返回一个空的集合。

3、getServletContext

public ServletContext getServletContext();
  • 返回这个Servlet 上下文对象的引用。

4、getServletName

 String getServletName();
  • 返回Servlet实例的名字。即标签下的 xxx

如何获取ServletConfig的对象?

  • 可通过调用Servlet接口的getServletConfig()方法来返回ServletConfig对象。

3、ServletRequest 和ServletResponse 接口

  • Servelt 由Servlet容器来管理,当客户请求到来时,容器创建一个ServletRequest对象封装请求数据,同时创建一个ServletResponse对象封装响应数据
  • 这两个对象被容器作为Service()方法的参数传递给Servlet,Servlet利用 ServletRequest对象获取客户端请求的数据,利用ServletResponse 对象发送响应数据。

ServletRequest接口中定义了很多方法。 其中一些如下:

编号方法描述
1public Object getAttribute(String name)返回以 name 为名字的属性的值,如果这个属性不存在,就返回null
2public Enumeration<String> getAttributeNames()返回请求中所有可用的属性的名字,如果没有,返回空的枚举集合。
3public void setAttribute(String name, Object object)在请求中保存名字为name 的属性。如果第二个参数 为null,相当于调用removeAttribute(name)。
4public void removeAttribute(String name)从请求中删除指定属性
5public String getParameter(String name)用于通过名称获取参数的值。
6public String[] getParameterValues(String name)返回一个包含给定参数名称的所有值的String数组。它主要用于获取多选列表框的值。
7java.util.Enumeration getParameterNames()返回所有请求参数名称的枚举。
8public int getContentLength()以字节为单位返回请求正文的长度,如果长度不知,返回-1
9public String getContentType()返回请求实体数据的网络媒体类型,如果未知则返回null
10public ServletInputStream getInputStream() throws IOException返回用于读取请求正文中二进制数据的输入流。javax.servlet.ServletInputStream 是一个抽象类,继承自java.io.InputStream。
11public BufferedReader getReader()返回BufferedReader对象,以字符数据方式读取请求正文
12public abstract String getServerName()返回接收请求的服务器的主机名。
13public int getServletPort()返回接收请求的服务器的端口号
14public String getRemoteAddr()返回发送请求者的IP地址。
15public String getRemoteHost()返回发送请求者的主机名称。如果引擎不能或者选择不解析主机名(为了改善性能),这个方法会直接返回IP地址。
16public int getRemotePort()返回请求者的IP源端口
17public String getCharacterEncoding()返回此请求输入的字符集编码。
18public void setCharacterEncoding()覆盖请求正文中使用的字符编码的名字
19public RequestDispatcher getRequestDispatcher(String path)返回一个RequestDispatcher对象
20public String getLocalAddr()返回接收到请求的网络接口的IP地址
21public String getLocalName()返回接收到请求的IP接口的主机名
22public String getLocalPort()返回接收到请求的网络接口的IP端口号

接下来看ServletResponse接口里的方法:

编号方法描述
1public String getCharacterEncoding()返回响应中发送的正文使用的字符编码(MIME字符集)
2public ServletOutputStream getOutputStream()返回一个ServletOutputStream 对象,用于在响应中写入二进制数据。javax.servlet.ServletOutputStream是一个抽象类,继承自java.io.OutputStream
3public PrintWriter getWriter()返回 PrintWriter对象,用于发送字符文本到客户端。PrintWriter对象使用getCharacterEncoding()方法返回的字符编码。如果没有指定响应的字符编码方式,默认将使用ISO-8859-1。
4public void setContentLength(int length)对于HTTP Servlet ,在响应中,设置内容正文的长度,这个方法设置HTTP Content-Length 实体报头。
5public void setContentType(String type)设置要发送到客户端的响应的内容类型,此时响应应该还没有提交。给出的内容类型可以包括字符编码说明,例如:text/html;charset=UTF-8

记忆技巧:从客户端和服务端交互过程思考,哪些对象应该是从请求对象中得到,哪些信息应该是用响应对象来设置!

4、ServletContext 接口

  • 运行在Java虚拟机中的每个web应用程序都有一个与之相关的Servlet上下文。Java Servlet API 提供了一个ServletContext 接口用来表示上下文。Servlet 可以使用这个接口里的方法与 Servlet容器进行通信。

  • ServletContext的对象由Web容器在部署项目时创建。 该对象可用于从web.xml文件获取配置信息。 每个Web应用程序只有一个ServletContext对象。 如果有信息要共享给多个servlet使用,最好在web.xml文件中使用<context-param>元素提供它。

ServletContext接口部分方法。

序号方法描述
1public String getInitParameter(String name)返回指定参数名称的参数值。
2public Enumeration getInitParameterNames()返回上下文的初始化参数的名称。
3public void setAttribute(String name,Object object)在应用程序范围内设置给定的对象。
4public Object getAttribute(String name)返回指定名称的属性。
5public Enumeration getInitParameterNames()返回上下文的初始化参数的名称,作为String对象的枚举。
6public void removeAttribute(String name)从servlet上下文中删除给定名称的属性。
7public String getRealPath(String path)返回一个符合URL路径格式的指定的虚拟路径的格式是:/dir/dir/filename.ext。
8public RequestDispatcher getRequestDispatcher(String uripath)返回RequestDispatcher对象

如何获取ServletContext接口的对象?

  • 通过ServletConfig接口的getServletContext()方法返回ServletContext对象。
  • 通过GenericServlet类的getServletContext()方法返回ServletContext对象。

5、GenericServlet 抽象类

之前我们是通过实现Servlet 接口来编写 Servlet 类,这需要实现 Servlet 接口中定义的五个方法,还需要维护手动维护 ServletConfig对象的引用。为了简化Servlet 的编写,在javax.servlet 包中提供了一个实现了 ServletServletConfig接口的抽象类 GenericServlet

public abstract class GenericServlet implements Servlet, ServletConfig,
        java.io.Serializable {

    private static final long serialVersionUID = 1L;
    private transient ServletConfig config;

    public GenericServlet() {
        // NOOP
    }

    @Override
    public void destroy() {
        // NOOP by default
    }

    @Override
    public String getInitParameter(String name) {
        return getServletConfig().getInitParameter(name);
    }

    @Override
    public Enumeration<String> getInitParameterNames() {
        return getServletConfig().getInitParameterNames();
    }

    @Override
    public ServletConfig getServletConfig() {
        return config;
    }

    @Override
    public ServletContext getServletContext() {
        return getServletConfig().getServletContext();
    }
            
    @Override
    public String getServletInfo() {
        return "";
    }

    @Override
    public void init(ServletConfig config) throws ServletException {
        this.config = config;
        this.init();
    }

    public void init() throws ServletException {
        // NOOP by default
    }

 
    public void log(String msg) {
        getServletContext().log(getServletName() + ": " + msg);
    }

    public void log(String message, Throwable t) {
        getServletContext().log(getServletName() + ": " + message, t);
    }

 
    @Override
    public abstract void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException;

    @Override
    public String getServletName() {
        return config.getServletName();
    }
}

可以看出其中大部分方法都是实现两个接口里的方法。如果我们需要编写一个通用的Servlet,只需要从GenericServlet类继承,并实现其中的抽象方法 service()。

GenericServlet类中定义了两个重载的 init()方法,下面我们来看一下由来。子类实现了GenericServlet,但是如果子类需要自己的初始化操作怎么办?

解决方案:让子类去重写init(ServletConfig config) 方法

public void init(ServletConfig config) throws ServletException{
    super.init(config);
    System.out,println("子类自己的初始化操作");
}

万一开发者逻辑思维能力不高,往往就会出现一些问题:比如在重写父类的init(ServletConfig config)方法时忘记调用父类的初始化方法(错误的)。

SUN公司的科学家早就考虑到了这个问题,专门写了一个没有参数的用于子类做初始化的方法init(),并暴露给子类用来初始化操作。

image-20210411095739045

6、HttpServlet 抽象类

我们编写的Servlet 主要应用于HTTP协议的请求和响应。为了快速开发应用于HTTP协议的Servlet类,Sun公司在 javax.servlet.http 包中给我们提供了一个抽象的类HttpServletHttpServlet类扩展了GenericServlet类并实现了Serializable接口。它提供了HTTP特定的方法,如:doGetdoPostdoHeaddoTrace等。

定义

public abstract class HttpServlet extends GenericServlet

在HttpServlet 类中提供了两个重载的 service()方法:

public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException 

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException 

第一个service()方法是GenericServlet类中的service()方法的实现。在这个方法中,首先将req 和resp 对象转化为 HttpServletRequest(继承自ServletRequest接口)和HttpServletResponse(继承自ServletResponse接口)类型,然后调用第二个service方法,对客户请求进行处理。

针对HTTP1.1 中定义的 7种请求方法 GETPOSTHEADPUTDELETETRACEOPTIONSHttpServlet分别提供了7个处理方法。

protected void doGet(HttpServletRequest req, HttpServletResponse res)//处理GET请求,它由Web容器调用。

protected void doPost(HttpServletRequest req, HttpServletResponse res)//处理POST请求,它由Web容器调用。

protected void doHead(HttpServletRequest req, HttpServletResponse res)//处理HEAD请求,它由Web容器调用。

protected void doOptions(HttpServletRequest req, HttpServletResponse res)//处理OPTIONS请求,它由Web容器调用。

protected void doPut(HttpServletRequest req, HttpServletResponse res)//处理PUT请求,它由Web容器调用。

protected void doTrace(HttpServletRequest req, HttpServletResponse res)//处理TRACE请求,它由Web容器调用。

protected void doDelete(HttpServletRequest req, HttpServletResponse res)//处理DELETE请求,它由Web容器调用。

7、HttpServletRequest 接口

定义:

public interface HttpServletRequest extends ServletRequest;    //用来处理一个对Servlet的HTTP格式的请求信息。

常用方法:

1String getMethod();	//返回请求方式:如GET/POST

2String getRequestURI();	//返回请求行中的资源名字部分:如/test/index.html

3StringBuffer getRequestURL();	//返回浏览器地址栏中所有的信息

4String getContextPath();	//返回当前项目的上下文路径,该值以/开头(<Context/>元素的path属性值.)

5String getRemoteAddr();	//返回发出请求的客户机的IP地址

6String getHeader(String name);//返回指定名称的请求头的值。

//获取请求参数的方法:
1String getParameter(String name);	//根据参数名获取单个参数值。

2String[] getParameterValues(String name);	//根据参数名获取多个参数值。

3Enumeration<String> getParameterNames();	//返回包含所有参数名的Enumeration对象。

4Map<String,String[]> getParameterMap();	//返回所有的参数名和值所组成的Map对象。

public java.lang.String getQueryString(); //返回请求URL 中在路径后的查询字符串。如果在URL中没有查询字符串,该方法返回null。例如,有如下的请求URL:http://localhost:8080/ch02/logon?action=logon调用getOueryString()方法将返回action=logon。

注意

  • ServletConfig中的getInitParameter方法是用于获取Servlet配置的初始化参数,该参数是程序员在开发的时候配置在web.xml中的
  • HttpServletRequest中的getParamter方法是用于获取客户端发送过来的参数,该参数是用户在发出请求携带的。

想要更好的理解HttpServletRequestHttpServletResponse的使用,请结合HTTP协议看。

扩展

因为Request代表请求,所以我们可以通过该对象分别获得HTTP请求的请求行,请求头和请求体。

image-20210411102359822
  • 通过request获得请求行
String getMethod()	//获得客户端的请求方式

//获得请求的资源:
String getRequestURI() //返回请求行中的资源名字部分:如/test/index.html
    
StringBuffer getRequestURL() 返回浏览器地址栏中所有的信息
    
String getContextPath() //返回当前项目的上下文路径,该值以/开头(<Context/>元素的path属性值.)
    
String getQueryString()	// 返回这个请求URL所包含的查询字符串。一个查询字串符在一个URL中由一个“?”引出。如果没有查询字符串,这个方法返回空值( 即get提交url地址后的参数字符串)
  • 通过request获得请求头
long getDateHeader(String name) //返回指定的请求头域的值,这个值被转换成一个反映自1970-1-1日(GMT)以来的精确到毫秒的长整数。如果头域不能转换,抛出一个IllegalArgumentException。如果这个请求头域不存在,这个方法返回-1。

String getHeader(String name)//返回一个请求头域的值。(译者注:与上一个方法不同的是,该方法返回一个字符串)如果这个请求头域不存在,这个方法返回-1。

Enumeration getHeaderNames()//该方法返回一个String对象的列表,该列表反映请求的所有头域名。有的引擎可能不允许通过这种方法访问头域,在这种情况下,这个方法返回一个空的列表。
    
int getIntHeader(String name)//  返回指定的请求头域的值,这个值被转换成一个整数。如果头域不能转换,抛出一个IllegalArgumentException。如果这个请求头域不存在,这个方法返回-1。

  • 通过request获得请求体
/*请求体中的内容是通过post提交的请求参数,格式是:

username=zhangsan&password=123&hobby=football&hobby=basketball

key ---------------------- value

username         [zhangsan]

password         [123]

hobby            [football,basketball]                                      */

//以上面参数为例,通过一下方法获得请求参数:
String getParameter(String name)

String[] getParameterValues(String name)

Enumeration getParameterNames()

Map<String,String[]> getParameterMap()

注意:get请求方式的请求参数 上述的方法一样可以获得。

乱码原因:

由于计算机中的数据都是以二进制形式存储的,因此,当传输文本时,就会发生字符和字节之间的转换。字符与字节之间的转换是通过查码表完成的,将字符转换成字节的过程称为编码,将字节转换成字符的过程称为解码,如果编码和解码使用的码表不一致,就会导致乱码问题。

7.1 Request中文乱码问题的解决方法
  • 在Tomcat服务器中,对于所有请求方式(GET/POST)的默认解码的方式都是ISO-8859-1
  • 浏览器使用的参数编码方式的UTF-8编码的,而服务器使用的是ISO-8859-1的方式来解码,结果中文肯定乱码

解决方案:

  1. 把字符串用ISO-8859-1的方式编码还原成byte数组
  byte[] data = username.getBytes("ISO-8859-1");
  1. 把该byte数组,使用UTF-8的方式来解码
  username = new String(data, "UTF-8");

但是,如果需要操作的参数过多,这个代码又要写N次。因此

针对GET方式:

  1. 以后开发中get方式一般不传参数,尤其不传中文参数

  2. server.xml文件中,配置默认处理URI的编码即可

image-20210411104405705

针对POS方式:

获取任意参数之前,设计请求体内容的编码 -> UTF-8

req.setCharacterEncoding("UTF-8");

8、HttpServletResponse 接口

  • ServletResponse接口:该接口的对象就表示响应对象,可以对请求作出响应操作

  • HttpServletResponse接口: 该接口对象可以处理带有HTTP类型的响应操作

定义

public interface HttpServletResponse extends ServletResponse //   描述一个返回到客户端的HTTP回应。这个接口允许Servlet程序员利用HTTP协议规定的头信息。
1public void addCookie(Cookie cookie);//在响应中增加一个指定的cookie。可多次调用该方法以定义多个cookie。为了设置适当的头域,该方法应该在响应被提交之前调用。

2public boolean containsHeader(String name);//检查是否设置了指定的响应头。
    
3public String encodeRedirectURL(String url);//对sendRedirect方法使用的指定URL进行编码。如果不需要编码,就直接返回这个URL。之所以提供这个附加的编码方法,是因为在 redirect的情况下,决定是否对URL进行编码的规则和一般情况有所不同。所给的URL必须是一个绝对URL。相对URL不能被接收,会抛出一个 IllegalArgumentException。所有提供给sendRedirect方法的URL都应通过这个方法运行,这样才能确保会话跟踪能够在所有浏览器中正常运行。

4public String encodeURL(String url);//对包含session ID的URL进行编码。如果不需要编码,就直接返回这个URL。Servlet引擎必须提供URL编码方法,因为在有些情况下,我们将不得不重写URL,例如,在响应对应的请求中包含一个有效的session,但是这个session不能被非URL的(例如cookie)的手段来维持。所有提供给Servlet的URL都应通过这个方法运行,这样才能确保会话跟踪能够在所有浏览器中正常运行。

5public void sendError(int statusCode) throws IOException;

public void sendError(int statusCode, String message) throws IOException;//用给定的状态码发给客户端一个错误响应。如果提供了一个message参数,这将作为响应体的一部分被发出,否则,服务器会返回错误代码所对应的标准信息。调用这个方法后,响应立即被提交。在调用这个方法后,Servlet不会再有更多的输出。

6public void sendRedirect(String location) throws IOException;//使用给定的路径,给客户端发出一个临时转向的响应(SC_MOVED_TEMPORARILY)。给定的路径必须是绝对URL。相对URL将不能被接收,会抛出一个IllegalArgumentException。这个方法必须在响应被提交之前调用。调用这个方法后,响应立即被提交。在调用这个方法后,Servlet不会再有更多的输出。

7public void setDateHeader(String name, long date);//用一个给定的名称和日期值设置响应头,这里的日期值应该是反映自1970-1-1日(GMT)以来的精确到毫秒的长整数。如果响应头已经被设置,新的值将覆盖当前的值。

8public void setHeader(String name, String value);//用一个给定的名称和域设置响应头。如果响应头已经被设置,新的值将覆盖当前的值。

9public void setIntHeader(String name, int value);//用一个给定的名称和整形值设置响应头。如果响应头已经被设置,新的值将覆盖当前的值。

10public void setStatus(int statusCode);//这个方法设置了响应的状态码,如果状态码已经被设置,新的值将覆盖当前的值。

注意:

  1. ServletOutputStream getOutputStream() : 获取字节输出流对象,该对象可以把数据输出到浏览器,文件下载时使用

  2. PrintWriter getWriter() : 获取和浏览相关的打印流对象 推荐使用

以上两个流对象,只能使用1个,不然报错

image-20210411105918838

8.1 Response中文乱码问题的解决方法
  1. 告诉浏览器数据的MIME类型
resp.setContentType("text/html");
  1. 告诉浏览器内容的编码
resp.setCharacterEncoding("utf-8");

以上两行代码可以合并,告诉浏览器数据的MIME类型和编码

resp.setContentType("text/html;charset=utf-8");

注意该操作必须在获取输出流对象之前操作,否则无效!

9、ResquestDispatcher 接口

ResquestDispatcher对象由Servlet容器创建,用于封装一个由路径识别的服务器资源。利用ResquestDispatcher对象,可以把请求转发给其他的 Servlet 或 JSP页面。

RequestDispatcher接口中定义了两种方法。它们分别是 :

编号方法描述
1public void forward(ServletRequest request,ServletResponse response)servlet的请求转发到服务器上的另一个资源(servletJSP文件或HTML文件)。
2public void include(ServletRequest request,ServletResponse response)在响应中包含资源的内容(servletJSP页面或HTML文件)。

下面来看两种方法的区别:

img

如上图所示,第一个 servlet转发请求到第二个 servle的响应发送给客户端。 第一个 servlet的响应不会显示给用户。

img

如上图所示,第二个 servlet的响应将包含在发送给客户端的第一个servlet的响应中。

3.3 Servlet 的继承体系

img

3.4 Servlet 的作用域对象

作用:可以共享数据。

  • request: 每一次请求的请求对象,如果在同一个请求内要共享数据,就可以使用请求转发来共享数据。

  • session: 每一次会话对象,如果在一次会话中,多个请求之间共享数据,就可以使用session来共享数据。

  • application

    • 整个web应用描述成的对象,一个web应用就是一个application对象,如果需要在多个会话中共享数据,就可以使用application对象。
    • web容器在启动的时候,它会为每个web程序都创建一个对应的ServletContext对象,它代表了当前的web应用
Servlet的作用域对象名称对象的类型
requestHttpServletRequest
sessionHttpSession
application(servletContext)ServletContext

3.5 Servelt 生命周期

Servlet 运行在 Servlet容器中,其生命周期由容器来管理。Servlet 的生命周期通过Servlet接口中的init()service()destory()方法来表示。

Servlet 的生命周期包含了下面四个阶段:

1、加载实例化

  • Servlet 容器负责加载和实例化Servlet。当Servlet 容器启动时,或者在当Servlet 容器接收到servlet的第一个请求时,类加载器负责加载Servlet 类。
  • 由于容器是通过反射来创建Servlet实例,调用的是Servlet的默认构造方法,因此我们在编写Servlet类时,要提供公共无参构造器。

2、初始化

  • **在Servlet 实例化后,容器会调用 Servlet 的init()方法来初始化这个对象。**初始化的目的是让Servlet 对象在处理客户端请求前完成一些初始化的工作,如建立数据库连接,获取配置信息等
  • 对于每个Servlet实例,init()方法只被调用一次。
  • 在初始化期间,可以通过ServletConfig 对象从 Web应用程序的配置信息(在web.xml中配置)中获取初始化的参数信息。

3、请求处理

  • 每当接收到servlet的请求时,Web容器都会调用 service方法。如果servlet未初始化,则遵循上述前三个步骤,然后调用service方法。 如果servlet被初始化,它调用service方法。 请注意,servlet仅初始化一次。

4、服务终止

  • 从服务中删除servlet实例之前,Web容器调用destroy方法。它使servlet有机会清理所有资源,例如内存,线程等

在整个 Servlet 的生命周期中,创建Servlet实例、调用实例的 init() 和destory() 方法都只进行一次,当初始化完成后, Servlet容器会将该实例保存在内存中,通过调用它的service() 方法,为接收到的请求服务。

Servlet 整个生命周期过程的UML时序图如下:

image-20210411151633612

提示:如果需要让 Servlet容器在启动时就加载 Servlet,可以在web.xml文件配置 元素。

3.6 Servlet 的执行流程

  1. Tomcat 解析请求为上下文路径(path)资源的名称(url-pattern)
  2. 根据上下文路径匹配server.xml 文件中的path的值,在根据docBase的值定位到项目的根路径
  3. 根据资源名称匹配WEB-INF/web.xml中的url-pattern,获取到Servlet的全限定名
  4. 判断是否存在这个类的缓存
    • 如果没有,就通过反射创建servlet对象,放入缓存。即加载实例化Servlet初始化init()请求处理service()
    • 如果有则调用service()方法传递请求和响应对象。
  5. Web容器在需要删除servlet时调用destroy方法,例如:在停止服务器或取消部署项目时。

3.7 servlet 的匹配顺序

当一个url与多个servlet的匹配规则可以匹配时,则按照 “ 精确路径 > 最长路径>扩展名”这样的优先级匹配到对应的servlet。举例如下:

1、精确路径匹配例

  • 例子:比如servletA 的url-pattern为 /test,servletB的url-pattern为 /* ,这个时候,如果我访问的url为http://localhost/test ,这个时候容器就会先进行精确路径匹配,发现/test正好被servletA精确匹配,那么就去调用servletA,也不会去理会其他的 servlet了。

2、最长路径匹配

  • 例子:servletA的url-pattern为/test/*,而 servletB的url-pattern为/test/a/*,此 时访问http://localhost/test/a时,容器会选择路径最长的servlet来匹配,也就是这里的servletB。

3、扩展匹配

  • 如果url最后一段包含扩展,容器将会根据扩展选择合适的servlet。例子:servletA的url-pattern:*.action

4、如果前面三条规则都没有找到一个servlet,容器会根据url选择对应的请求资源。如果应用定义了一个default servlet,则容器会将请求丢给default servlet“/” 是用来定义default servlet映射的,default servlet是tomcat内置的

3.8 url-pattern详解

< url-pattern>/</url-pattern>  <!--会匹配到/login这样的路径型url,不会匹配到模式为*.jsp这样的后缀型url-->

< url-pattern>/*</url-pattern> <!--会匹配所有url:路径型的和后缀型的url(包括/login,*.jsp,*.js和*.html等)-->

首先无论是 / 还是 /*,springMVC都会拦截所有请求,

  • 当使用 / 时,会覆盖默认处理静态资源的Servlet,根据上面的匹配顺序,会匹配到/login这样的路径型url,不会匹配到模式为 .jsp 这样的后缀型url。说到为什么JSP页面的请求并不会命中这个servlet,那是因为servlet容器内建的JSP servlet将会被调用,而这个容器内建的JSP servlet已经默认地映射在了*.jsp上。
  • 当使用 /* 时,会匹配所有url:路径型的和后缀型的url(包括/login,*.jsp,*.js和*.html等),会出现返回 jsp视图 时再次进入spring的DispatcherServlet 类,导致找不到对应的controller所以报404错。

在项目中我们定义的web.xml是访问动态资源的。

在tomcat内置的有两种servlet,一种是DefaultServlet,另一种是JspServlet

    <!-- 默认JSP Servlet -->
	<servlet>
        <servlet-name>jsp</servlet-name>
        <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
        <init-param>
            <param-name>fork</param-name>
            <param-value>false</param-value>
        </init-param>
        <init-param>
            <param-name>xpoweredBy</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>3</load-on-startup>
    </servlet>
	<!--默认default Servlet-->
    <servlet>
        <servlet-name>default</servlet-name>
        <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
        <init-param>
            <param-name>debug</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <param-name>listings</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

	<!-- The mapping for the default servlet -->
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <!-- The mappings for the JSP servlet -->
    <servlet-mapping>
        <servlet-name>jsp</servlet-name>
        <url-pattern>*.jsp</url-pattern>
        <url-pattern>*.jspx</url-pattern>
    </servlet-mapping>
  • default的servlet是解读静态页面的,这种方法使用映射规则的是

    • <url-pattern>/</url-pattern>
      
  • 而jsp的servlet是解读动态界面的,这个方法使用映射规则的是

    • <url-pattern>*.jsp</url-pattern><url-pattern>*.jspx</url-pattern>
      

注意:

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

当我们配置自己的Servlet映射时,使用这种方式会覆盖默认的DefaultServlet,使原本访问静态资源变成了访问动态资源,导致无法访问静态页面。因此在springmvc.xml文件中添加 < mvc:default-servlet-handler >,将静态资源交给默认的Servlet去处理,可以避免这个问题。

3.9 forward() 和 sendRedirect()方法的区别

image-20210412090139475

从图中可以看出调用 sendRedict() 方法,实际上是告诉浏览器 Servlet2 所在位置,让浏览器重新访问Servlet2。整个过程是对用户是透明的,浏览器自动完成。且浏览器地址栏显示的URL是重定向之后的URL。

image-20210412090152780

从图中可以看出调用forward()方法,对浏览器来说是透明的,浏览器并不知道为其服务的Servlet已经换成Servlet2了,它只知道发出一个请求,获得了一个响应。且浏览器地址栏显示的URL始终是原始请求的URL。

sendRedirect() 方法回容forward() 方法还有一个区别,那就是sendRedirect()方法不但可以在位于同一主机上的不同Web应用程序之间重定向,而且可以将客户端重定向到其他服务器的Web应用程序资源。

转发和重定向的区别总结:

1、请求次数不同,重定向两次,转发一次

2、重定向时地址栏会发生变化,而转发时地址栏不会发生变化

3、重定向两次请求不共享数据,转发一次请求共享数据

4、重定向时的网址可以是任何网址,转发的网址必须是本站点的网址

扩展:表单重复提交

表单重复提交是三种方式:

  • 用户提交表单后,刷新地址栏
    • 解决:重定向
  • 网络延迟,用户再次点击提交按钮
    • 禁用按钮
  • 用户提交表单后,点击浏览器的【后退】按钮回退到表单页面后进行再次提交
    • token

【重复提交表单】表单重复提交的三种情况,解决办法

3.10 cookie 和 session

1、会话跟踪

在 Java Servlet API中,javax.servlet.http.HttpSession接口封装了Session的概念。Servlet容器提供了这个接口的实现。当请求一个会话的时候,Servlet容器就会创建一个HttpSession对象,这个对象可以用来保存客户的状态信息。Servlet容器为 HttpSession对象分配一个唯一的Session ID,将其作为Cookie发送给浏览器,浏览器在内存中保存这个cookie。当客户再次发送HTTP请求时,浏览器将Cookie随请求一起发送,Servlet容器从请求对象中读取Session ID,然后根据 Session ID找到对应的HttpSession对象,从而获得客户的状态信息。

image-20210415141101778

2、HttpSession 接口

如何获取HttpSession对象?

HttpServletRequest接口提供了两种获取HttpSession对象的方法:

  • public HttpSession getSession():返回与此请求相关联的当前会话,或者如果请求没有会话,则创建一个会话。
  • public HttpSession getSession(boolean create): 返回与此请求相关联的当前HttpSession,如果没有当前会话,并且create的值为true,则返回一个新会话。

HttpSession接口的常用方法

  1. public Object getAttribute(String name)

  2. public Enumeration getAttributeNames()

  3. public void removeAttribute(String name)

  4. public void setAttribute(String name, Object value)

上面四个方法用于在 HttpSession对象中读取、移除和设置属性,利用这些方法,可以在Session中维护客户的状态信息。

  1. public String getId() : 返回一个包含唯一标识符值的字符串。

  2. public long getCreationTime() : 返回创建此会话的时间,以1970年1月1日GMT格林尼治时间以来的毫秒为单位。

  3. public long getLastAccessedTime() : 返回客户端发送与此会话相关联的请求的最后一次,为1970年1月1日GMT以来的毫秒数。

  4. public void invalidate() :使此会话无效,然后取消绑定绑定到该对象的任何对象。

  5. public boolean isNew():如果客户端还不知道这个Session或者客户端没有选择加入Session,那么这个方法将返回true。例如,服务器使用基于Cookie的Session,而客户端禁用了Cookie,那么对每一个请求,Session都是新的。

3、cookie和session的区别

cookie:是客户端技术,因为HTTP请求时无状态的,所以服务器会把数据以cookie的形式返回给浏览器,当再次访问的时候浏览器就带着cookie来访问。

session:是服务端技术,服务端会为每个用户都创建一个独享的session对象,服务端将JSESSIONID通过的cookie返回给浏览器,当再次访问时,浏览器就根据JSESSIONID去session对象取数据,安全可靠。

3.11 Servlet 的异常处理机制

1、声明式异常处理

声明式异常处理是在web.xml文件中声明对各种异常的处理办法。这是通过<erreo-page>元素来声明的。

image-20210415151208452
  • error-code 元素指定HTTP错误代码
  • exception-type 元素指定Java异常类的完整限定名
  • location元素指定用于响应HTTP错误代码或者Java异常的资源的路径,该路径是相对于Web应用程序的根路径的位置,必须以斜杠(/) 开头
HTTP 错误代码的处理

当我们访问404,找不到资源的时候,就会跳转到error.html页面。

<error-page>
    <error-code>404</error-code>
    <location>/error.html</location>
</error-page>

除此之外,我们还可以写一个专门用来处理HTTP错误代码的类,例如 HttpErrorHandleServlet来处理。在web.xml文件中配置该类

<servlet>
    <servlet-name>HttpErrorHandleServlet</servlet-name>
    <servlet-class>com.zxc.Controller.HttpErrorHandleServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>HttpErrorHandleServlet</servlet-name>
    <url-pattern>/HttpErrorHandle</url-pattern>
</servlet-mapping>

<error-page>
    <error-code>404</error-code>
    <location>/HttpErrorHandle</location>
</error-page>

当发生HTTP错误的时候,Servlet容器会自动将HTTP错误代码作为javax.servlet.error.status_code属性的值,保存到请求对象中。为了帮助进行错误处理的Servlet分析问题,以及生成详细的响应,Servlet容器在将请求转发给错误页面之前,会在请求中设置某些有用的属性

image-20210415152405633
Java异常的处理

利用<error-page> 元素还可以声明对程序中产生的Java异常处理。

<servlet>
    <servlet-name>ExceptionHandleServlet</servlet-name>
    <servlet-class>com.zxc.Controller.ExceptionHandleServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>ExceptionHandleServlet</servlet-name>
    <url-pattern>/ExceptHandle</url-pattern>
</servlet-mapping>

<error-page>
    <exception-type>java.io.FileNotFoundException</exception-type>
    <location>/ExceptHandle</location>
</error-page>

2、程序式异常处理

就是在Web程序中利用 try-catch 语句来捕获异常,并进行处理。

3.12 Servlet 的线程安全

1、多线程的 Servlet模型

Servlet 容器只产生一个Servlet实例,如果多个客户请求同时访问这个 Servlet,Servlet容器将采用多线程。容器维护了一个线程池来服务请求。

  • 线程池实际上等待执行代码的一组线程,这些线程叫做工作线程(Worker Thread)
  • Servlet容器使用一个调度线程(Dispatcher Thread)来管理工作线程

当容器接受到请求时,调度线程从线程池中取一个工作线程,将请求传递给该线程,然后由其执行service()方法。

image-20210415185846535

当这个线程正在执行的时候,容器收到了另外一个请求,调度者线程将从池中选取另一个线程来服务新的请求。要注意的是,Servlet容器并不关心这第二个请求是访问同Servlet还是另一个Servlet。因此,如果容器同时收到访问同一个Servlet的多个请求,那么这个Servlet 的 service()方法将在多个线程中并发地执行。图7-2显示了两个工作者线程都在执行同一个Servlet的service()方法。

image-20210415190026137

2、总结

Servlet 规范定义,在默认情况下,Servlet是多线程的,一个 Servlet实例同时在多个线程中执行(单实例多线程),并发地处理多个客户端请求。因为 Servlet 是多线程的,所以在开发Servlet时,要注意线程安全的问题。

  • 变量的线程安全问题:本地变量总是线程安全的,实例变量和类变量不是线程安全的

  • 属性的线程安全问题:请求对象的属性访问是线程安全的,Session对象和上下文对象的属性访问不是线程安全的

建议:

  • 尽可能地在Servlet 中只使用本地变量。
  • 应该只使用只读的实例变量和静态变量。
  • 不要在 Servlet 中创建自己的线程。
  • 修改共享对象时,一定要使用同步,尽可能地缩小同步代码的范围,不要直接在service()方法或doXXX(方法上进行同步,以免影响性能。
  • 如果在多个不同的Servlet中,要对外部对象(例如,文件)进行修改操作,一定要加锁,做到互斥的访问。

3.13 Filter

  • 过滤器,就是在源数据和目的数据之间起过滤作用的中间组件,类比于生活中的污水净化器。
  • 而对于Web应用程序来说,过滤器是一个驻留在服务器端的Web组件,它可以截取客户端和资源之间的请求和响应信息,并对这些信息进行过滤。

1、拦截器与过滤器的区别

1、拦截器是基于java的反射机制的,而过滤器是基于函数的回调。

2、拦截器不依赖于servlet容器,而过滤器依赖于servlet容器。

3、拦截器只对action请求起作用,而过滤器则可以对几乎所有的请求起作用。

4、拦截器可以访问action上下文、值、栈里面的对象,而过滤器不可以。

5、在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。

6、拦截器可以获取IOC容器中的各个bean,而过滤器不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑

2、过滤器的使用

  • 记录所有传入的请求
  • 记录来自标计算机的IP地址的请求
  • 转变/转换
  • 数据压缩
  • 加密和解密
  • 输入验证等

3、过滤器的API

与过滤器开发相关的接口与类都包含在javax.servletjavax.servlet.http包中,主要有下面的接口和类:

  • javax.servlet.Filter接口
  • javax.servlet.FilterConfig接口
  • javax.servlet.FilterChain接口
  • javax.servlet.ServletRequestWrapper
  • javax.servlet.ServletResponseWrapper
  • javax.servlet.http.HttpServletRequestWrapper
  • javax.servlet.http.HttpServletResponseWrapper

与开发 Servlet要实现javax.servlet.Servlet 接口类似,开发一个过滤器,必须要实现javax.servlet.Filter接口,并提供一个公共无参构造器。Filter接口为过滤器提供了生命周期方法如下。

方法描述
public void init(FilterConfig config)init()方法只被调用一次,用于初始化过滤器。容器在调用该方法时,向过滤器传递FilterConfig对象,FilterConfig是用法和ServletConfig类似
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)该方法类似于 Servlet接口的Service()方法,当用户请求过滤器所映射到资源时,会调用doFilter()方法,它用于执行过滤任务。在特定操作完成后,可以调用chain.doFilter(res, resp)将请求传递给下一个过滤器。也可以直接向客户端返回响应信息,或者利用forward()和include()方法,以及sendRedirect()方法将请求转向到其他资源。需要注意的是,这个方法的请求和响应参数的类型是ServletRequestServletResponse,也就是说,过滤器的使用并不依赖于具体的协议。
public void destroy()当过滤器从服务中取出时,调用此方法(仅一次)。

4、FilterConfig接口

  • FilterConfig接口ServletConfig接口方法差不多,不做过多阐述。

5、FilterChain 接口

方法描述
public void doFilter(ServletRequest request,ServletResponse response)调用该方法将使过滤器链中的下一个过滤器被调用。如果调用该方法的过滤器是链中最后一个过滤器,那么目标资源被调用。

6、四个包装类

HttpServletRequest 类没有提供的对请求信息进行修改的 setXXX()方法,而HttpServletResponse 类也没有提供得到响应数据的方法。也就是虽然过滤器可以截取到请求和响应对象,但是却无法直接使用这两个对象对它们的回数据进行替换。

虽然不能直接修改,但是可以利用请求和响应的包装类,来间接改变请求和响应的信息。在Servlet规范中,定义了 4 个包装类:ServletRequestWrapperServletResponseWrapperHttpServletRequestWrapperHttpServletResponseWrapper。它们都实现了请求和响应的接口。

它们在构造方法中接受真正的请求或响应对象,利用该对象的方法来完成自己需要实现的方法。包装类是装饰设计模式的运用。我们只需要编写一个包装类的子类,然后覆盖想要修改的方法就可以了。

7、过滤器执行顺序

image-20210412221635404

8、案例:字符编码过滤

<!--配置字符编码过滤器  -->
<filter>
    <filter-name>characterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>utf-8</param-value>		
    </init-param>
</filter>
<filter-mapping>
    <filter-name>characterEncodingFilter</filter-name>
    <url-pattern>*.do</url-pattern>
</filter-mapping>

9、其他案例

  • Servlet身份验证过滤器
  • 《Servlet JSP深入详解》16.5 对用户进程统一验证的过滤器
  • 《Servlet JSP深入详解》16.6 对请求和响应数据进行替换的过滤器
  • 《Servlet JSP深入详解》16.7 对响应内容进行压缩的过滤器

3.14 Listener

  • Listener就是监听器,我们在JavaSE开发或者Android开发时,经常会给按钮加监听器,当点击这个按钮就会触发监听事件,调用onClick方法,本质是方法回调。
  • Servlet API中定义了8个监听器接口,可以用监听ServletContextHttpSessionServletRequest对象的生命周期事件,以及这些对象的属性改变事件。

应用域监听

1、ServletContext(监听Application)

  • 生命周期监听ServletContextListener,它有两个方法,一个在出生时调用,一个在死亡时调用;
void contextInitialized(ServletContextEvent sce)//当Web应用程序初始化进程正开始时,Web容器调用这个方法。该方法将在所有的过滤器和Servlet初始化之前被调用。

void contextDestroyed(ServletContextEvent sce)//当Servlet上下文将要被关闭时,Web容器调用这个方法。该方法将在所以的 Servle和过滤器销毁之后被调用。

Servlet 容器通过ServletContextEvent对象来通知实现了 ServletContextListener接口的对象,该对象可以利用 ServletContextEvent对象得到ServletContext对象。

javax.servlet.ServletContextEvent类除了构造方法外,只定义了一个方法,就是 public ServletContext getServletContext();该方法用于得到ServletContext对象。

  • 属性监听ServletContextAttributeListener,它有三个方法,一个在添加属性时调用,一个在替换属性时调用,最后一个是在移除属性时调用。
void attributeAdded(ServletContextAttributeEvent event)//添加属性时;

void attributeReplaced(ServletContextAttributeEvent event)//替换属性时;

void attributeRemoved(ServletContextAttributeEvent event)//移除属性时;

2、HttpSession(监听Session)

  • 生命周期监听HttpSessionListener,它有两个方法,一个在出生时调用,一个在死亡时调用;
void sessionCreated(HttpSessionEvent se)//创建session时

void sessionDestroyed(HttpSessionEvent se)//销毁session时

Servlet 容器通过HttpSessionEvent对象来通知实现了 HttpSessionListener接口的对象,该对象可以利用 HttpSessionEvent对象得到HttpSession对象。

javax.servlet.http.HttpSessionEvent类除了构造方法外,只定义了一个方法,就是 public HttpSession getSession();该方法用于得到HttpSession对象。

  • 属性监听HttpSessioniAttributeListener,它有三个方法,一个在添加属性时调用,一个在替换属性时调用,最后一个是在移除属性时调用。
void attributeAdded(HttpSessionBindingEvent event)//添加属性时;

void attributeReplaced(HttpSessionBindingEvent event)//替换属性时

void attributeRemoved(HttpSessionBindingEvent event)//移除属性时
监听器接口方法说明
javax.servlet.http.HttpSessionActivationListenersessionWillPassivate sessionDidActivate实现这个接口,如果绑定到Session中,当Session被钝化或激活时,Servlet容器将通知该对象
javax.servlet.http.HttpSessionBindingListenervalueBound valueUnbound如果想让一个对象在绑定到Session中或者从Session中被删除时得到通知,那么可以让这个对象实现该接口

HttpSessionAttributeListenerHttpSessionBindingListener接口的主要区别是:

  • 前者用于监听Session中何时添加、删除或者替换了某种类型的属性,
  • 而后者是由属性自身来实现,以便属性知道它何时添加到一个Session中,或者何时从Session中被删除

3、ServletRequest(监听Request)

  • 生命周期监听ServletRequestListener,它有两个方法,一个在出生时调用,一个在死亡时调用;
void requestInitialized(ServletRequestEvent sre)//创建request时

void requestDestroyed(ServletRequestEvent sre)//销毁request时
  • 属性监听ServletRequestAttributeListener,它有三个方法,一个在添加属性时调用,一个在替换属性时调用,最后一个是在移除属性时调用。
void attributeAdded(ServletRequestAttributeEvent srae)//添加属性时

void attributeReplaced(ServletRequestAttributeEvent srae)//替换属性时

void attributeRemoved(ServletRequestAttributeEvent srae)//移除属性时

4、案例:HttpSessionBindingListener

如果一个对象实现了该接口,当该对象被绑定到 session中 或者从 session中删除时,容器会通知该对象,而该对象接收到通知时可以做一些初始化或清除状态的操作。

Servlet 容器通过HttpSessionBindingEvent对象来通知实现了 HttpSessionBindingListener 接口的对象,该对象可以利用 HttpSessionBindingEvent对象得到HttpSession对象。

HttpSessionBindingEvent类如下:

public class HttpSessionBindingEvent extends HttpSessionEvent {

    /* The name to which the object is being bound or unbound */
    private final String name;

    /* The object is being bound or unbound */
    private final Object value; 
    
	public HttpSessionBindingEvent(HttpSession session, String name) {
        super(session);
        this.name = name;
        this.value = null;
    }

 
    public HttpSessionBindingEvent(HttpSession session, String name,
            Object value) {
        super(session);
        this.name = name;
        this.value = value;
    }
	//上面两个构造方法构造一个事件对象,当一个对象被绑定到Session中或者从Session中被删除时,用这个事件对象来通知它。

    @Override
    public HttpSession getSession() {
        return super.getSession();
    }

    public String getName() {
        return name;
    }

  
    public Object getValue() {
        return this.value;
    }
}

5、案例:HttpSessionBindingListener 统计在线人数!

利用HttpSessionBindingListener 接口,编写一个统计在线人数。

  • 当一个用户登录后,显示当前在线人数和用户名单。当一个用户退出登录或者Session超时值发生,从在线用户名单删除该用户。
  • 这个功能注意利用实现了HttpSessionBindingListener 接口的对象,当这个对象绑添加到session中或从session中删除时,更新当前在线用户名单。

《Servlet JSP深入详解》15.4 在线人数统计程序

6、案例:ServletContextListener在Spring中的应用

Spring容器可以显示的实例化一个Spring IOC容器

ApplicationContext ctx = new ClassPathXmlApplicationContext("配置文件的路径")

也可以在web.xml配置文件中注册Spring IOC容器

<listener>
	<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value>
</context-param>

其中的监听器类【org.springframework.web.context.ContextLoaderListener】实现了ServletContextListener接口,能够监听ServletContext的生命周期中的“初始化”和“销毁”。这个监听器是Spring提供的。

当ServletContext初始化后,Spring IOC容器是如何初始化的呢?

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
    public ContextLoaderListener() {
    }
    public ContextLoaderListener(WebApplicationContext context) {
        super(context);
    }
--------------------------------------------------------重点关注下面这里哦!--------------------------------

    public void contextInitialized(ServletContextEvent event) {
        this.initWebApplicationContext(event.getServletContext());
    }
--------------------------------------------------------重点关注上面这里哦!--------------------------------

    public void contextDestroyed(ServletContextEvent event) {
        this.closeWebApplicationContext(event.getServletContext());
        ContextCleanupListener.cleanupAttributes(event.getServletContext());
    }
}

从代码中可以看出是通过ContextLoaderListener父类ContextLoader里的方法 this.initWebApplicationContext(event.getServletContext());来进行初始化 Spring IOC容器的

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
 
    if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
 
        throw new IllegalStateException("Cannot initialize context because there is already a root application context present - check whether you have multiple ContextLoader* definitions in your web.xml!");
 
    } else {
        Log logger = LogFactory.getLog(ContextLoader.class);
        servletContext.log("Initializing Spring root WebApplicationContext");
        
        if (logger.isInfoEnabled()) {
            logger.info("Root WebApplicationContext: initialization started");
        }
        long startTime = System.currentTimeMillis();
        try {
            if (this.context == null) {
                this.context = this.createWebApplicationContext(servletContext);
            }
 
            if (this.context instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)this.context;
                if (!cwac.isActive()) {
                    if (cwac.getParent() == null) {
                        ApplicationContext parent = this.loadParentContext(servletContext);
                        cwac.setParent(parent);
                    }
                    this.configureAndRefreshWebApplicationContext(cwac, servletContext);
                }
            }

            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

            ClassLoader ccl = Thread.currentThread().getContextClassLoader();
 
            if (ccl == ContextLoader.class.getClassLoader()) {
                currentContext = this.context;
            } else if (ccl != null) {
              	currentContextPerThread.put(ccl, this.context);
            }
            if (logger.isDebugEnabled()) {
                logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" + WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
            }
            if (logger.isInfoEnabled()) {
                long elapsedTime = System.currentTimeMillis() - startTime;
                logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
            }
            return this.context;
        } catch (RuntimeException var8) {
 
            logger.error("Context initialization failed", var8);
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, var8);
            throw var8;
        } catch (Error var9) {
            
            logger.error("Context initialization failed", var9);
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, var9);
             throw var9;
         }
    }
}

此时SpringIOC容器已被实例化。

总结:

  • 当Servlet容器启动时,ServletContext对象被初始化,然后Servlet容器调用web.xml中注册的监听器的public void contextInitialized(ServletContextEvent event)方法
  • 在该方法中,调用了this.initWebApplicationContext(event.getServletContext())方法,在这个方法中实例化了Spring IOC容器。即ApplicationContext对象。
  • 因此,当ServletContext创建时我们可以创建applicationContext对象,当ServletContext销毁时,我们可以销毁applicationContext对象。这样applicationContext就和ServletContext“共生死了”。

3.15 国际化 i18n

国际化(Internationalization)是使程序在不做任何修改的情况下,就可以在不同的国家地区和不同的语言环境下,按照当地的语言和格式习惯显示字符。国际化又被称为I18N,因为国际化的英文的是Internationlization,它以I开头 ,N结尾,中间有18个字母。

在Java中编写国家化程序主要通过两个类来完成,java.util.Locale类 和java.util.ResourceBundle抽象类。

  • Locale类用于提供本地信息,通常称为语言环境。不同语言、不同国家和地区采用不同的 locale对象来表示。
  • ResourceBundle类称为资源包,包含了特定于语言环境的资源对象

1、Locale类 语言环境

java.util.Locale 类的常用构造方法如下:

  • public Locale(String language)
  • public Locale(String language, String country)

其中 language表示语言,取值由ISO-639定义的小写的、两个字母组成的代码。

语言代码
汉语(Chinese)zh
英语(English)en
德语(German)de
法语(French)fr
日语(Japanese)ja
朝鲜语(Korean)ko

其中 country 表示国家和地区,取值由ISO-3166定义的大写的、两个字母组成的代码。

国家(地区)代码
中国(China)CN
美国(United States)US
英国(Great Britain)GB
德国(Germany)DE
日本(Japan)JP
韩国(Korea)KR

例如

  • 应用于中国的 Locale
Locale locale = new Locale("zh", "CN");
  • 应用于美国的 Locale
Locale locale = new Locale("en", "US");
  • 英语于应该的 Locale
Locale locale = new Locale("en", "GB");

为了简化,Locale类中定义了许多Locale对象常量。

应用国家或地区的Locale对象有:

  • Locale.CANADA
  • Locale.CHINA
  • Locale.FRANCE
  • Locale.JAPAN
  • Locale.US
  • Locale.HK
  • Locale.TAIWAN

应用于语言的Locale对象(只设定语言,没有设定国家和地区)

  • Locale.CHINESE
  • Locale.ENGLISH
  • Locale.JAPANSE

另外,在Locale类中,还定义了一个静态的方法getDefault()用于获得本地系统默认的Locale对象。

2、ResourceBundle类 资源包

要获取某个资源包,可以调用java.util.ResourceBundle类中的静态方法getBundle()

  • public static final ResourceBundle getBundle(String baseName); 根据基名得到资源包,使用系统默认的Locale对象。

  • public static final ResourceBundle getBundle(String baseName, Locale locale); 根据基名和Locale对象得到资源包。

利用getBundle() 方法可以得到对应于某个Locale对象的资源包,然后就可以利用 ResourceBundle类的getString()方法得到相应语言版本的字符串。

  • public final String getString(String key)

为了简化资源包的简写,从ResourceBundle类派生出来了两个资源类:ListResourceBundlePropertyResourceBundle

  • 使用ListResourceBundle类,需要编写自己的资源包继承此类,然后将所有的数据放入一个对象数组即可。

  • 使用 PropertyResourceBundle类,把数据放在属性文件中(properties),根据选择不同的国家/地区,来读取不同的配置文件,该配置文件名称必须符合规则:基本名称_语言_国家/地区.properties

加载资源,可以调用 ResourceBundle 类的静态方法getBundle(),getBundle()方法首先去加载资源类;如果没有成功,则试着去加载属性资源文件,如果成功,则创建一个新的PropertyResourceBundle对象。

案例
image-20210417100753033
ResourceBundle bundle = ResourceBundle.getBundle("app",Locale.CHINA);
String username = bundle.getString("username");
String password = bundle.getString("password");
System.out.println(username);
System.out.println(password);

==注意: 此处要将配置文件方在src目录下!!!==不然报 java.util.MissingResourceException: Can‘t find bundle for base name app, locale zh_CN

image-20210417103152972

输出:

image-20210417103231700

3、MessageFormat消息格式化

String msg = "我是{1},你是{0},他是{3},它是{2}";
msg = MessageFormat.format(msg, "one","two","three","four");
System.out.println(msg);
//output
//我是two,你是one,他是four,它是three

format()方法参数的顺序,是和占位符的数字顺序相对应的,而不是与占位符出现在消息文本的顺序对应

3.16 文件上传下载

1、文件上传

步骤

  1. 表单的method属性值必须是POST,由于GET方式提交是数据大小不能超过2KB,而POST是没有限制的

    <form action="/upload" method="POST">
    
  2. 需要使用到上传控件,该上传控件的type属性必须是file

     <input type="file" name="headImg"/>
    
  3. 由于上传的内容中有图片,图片是属于二进制类型的数据,不能进行字符编码,修改表单的编码方式,改成使用二进制的方式来编码

    <form action="/upload" method="POST" enctype="multipart/form-data">
    <!--表单 enctype 属性的默认值 application/x-www-form-urlencoded -->
    

注意

使用multipart/form-data编码后就不能使用req.getParamter()方法来获取参数了,该方法只能获取到URLEncoder编码的参数,multipart/form-data是表单的二进制编码,要用getInputStream() 方法来得到输入流,然后从输入流中读取传送的内容,再根据文件上传的格式进行分析,取出长传文件的内容和表单中其他字段的内容。

文件上传格式分析

image-20210418150353250
  • CRLF表示回车换行。

  • boundary表示传送内容的分隔符,这是随机的,不同浏览器采用的分隔符形式也是不同的。

commons-fileupload 组件

Commons是 Apache 开放源代码组织中的一个 Java子项目,该项目涉及文件上传、命令行处理、数据库连接池、XML配置文件处理等。commons-fileupload就是用来处理基于表单的文件上传的组件。该组件还依赖Apache 的另一个项目commons-io

commons-fileupload 组件中,主要用到下面一个接口两个类:

  • FileItem
  • DiskFileItemFactory
  • SevletFileUpload

SevletFileUpload 负责处理上传的文件数据,并将每部分的数据封装到一个 FileItem对象中。FileItem是一个接口,DiskFileItem类继承了此接口,上传的数据封装到了DiskFileItemDiskFileItemFactory 用来创建 FileItem 对象的工厂类,在这个工厂类中可以配置内存缓冲区大小和存放临时文件的目录。

FileItem接口中定义的主要方法如下:

方法描述
public byte[] get()以字节数组的形式返回文件数据项的内容
public String getContentType()返回客户端浏览器设置的文件数据项的MIME类型
public String getFieldName()返回文件数据项对应的表单中的字段的名字
public InputStream getInputStream()返回一个输入流,通过这个输入流来读取文件的内容
public String getName()返回在客户端文件系统中文件的原始文件名,这是由客户端的浏览器提供的
public OutputStream getOutputStream()返回一个输出流,利用这个输出流可以存储文件的内容
public long getSize()返回文件数据项的大小
public String getString()使用默认的字符编码,以字符串的形式返回文件数据项的内容
public String getString(String charset)使用指定的编码方式,以字符串的形式返回文件数据项的内容
public boolean isFormField()判断FileItem对象是否表示了一个简单的表单字段。
public void write(File file)将文件数据项的内容写到硬盘上。

案例

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>上传</title>
</head>
<body>
<div style="text-align:center;">

    <form action="/upload" method="post" enctype="multipart/form-data">
        账号<input type="text" name="username"><br/>
        邮箱<input type="text" name="email"><br/>
        头像<input type="file" name="headImage"><br/>
        <input type="submit" value="提交">
    </form>

</div>
</body>
</html>

UploadServlet.java

@WebServlet("/upload")
public class UploadServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {    
        try {
            // 检查当前请求是否符合文件上传的规定 menthd=post enctype=multipart/form-data
            boolean isMultipart = ServletFileUpload.isMultipartContent(req);
            if (isMultipart) {
                // 创建一个磁盘文件工厂,用来创建DiskFileItem
                DiskFileItemFactory factory = new DiskFileItemFactory();
                // 创建一个文件处理上传对象
                ServletFileUpload upload = new ServletFileUpload(factory);
                // 解析上传的文件流,得到FileItem 对象的列表
                List<FileItem> items = upload.parseRequest(req);
                // 遍历
                for (FileItem item : items) {
                    // 获取表单中的字段名称,即<input type="text" name="username">中的 username
                    String name = item.getFieldName();
                    //判断FileItem对象是否表示了一个简单的表单字段
                    if (item.isFormField()) {
                        // 获取表单字段填写的值,即用户输入的值
                        String value = item.getString("UTF-8");
                        System.out.println("name:" + name + " value:" + value);//name:username value:123
                    } else {
                        //在IE中获取到的是文件的绝对路径,
                        //而在W3C浏览器中获取到的是文件的简单名称,即文件上传的名称,即图片的名称 xxx.png
                        name = item.getName();
                        //获取表单中字段的名称,即headImage
                        String name1 = item.getFieldName();
                        //避免IE浏览器获得绝对路径,使用commons.io.FilenameUtils的getname()方法,获取文件的名称
                        String fileName = FilenameUtils.getName(name);
                        //P80520-150121.jpg----------headImage------------P80520-150121.jpg
                        System.out.println(name + "----" + name1+"--------"+fileName);
                        // 避免文件名称相同会发生文件覆盖的问题,使用UUID的值来替代文件的名称,getExtension方法获取文件的扩展名
                        fileName = UUID.randomUUID().toString() + "." + FilenameUtils.getExtension(item.getName());
                        //为方便能访问到上传的文件,这些文件应该放在项目中,在web根路径下建一个upload目录
                        String dir = req.getServletContext().getRealPath("upload");
                        item.write(new File(dir, fileName));
                    }
                }

            }
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

commons.io包里的工具类FilenameUtils里的常用方法:

String path= "F:\\FileUpload\\image\\1.png";
//获取文件的名称没有拓展名
System.out.println(FilenameUtils.getBaseName(path));//1
//获取文件的拓展名
System.out.println(FilenameUtils.getExtension(path));//png
//获取文件的名称
System.out.println(FilenameUtils.getName(path));//1.png
//获取文件的前缀
System.out.println(FilenameUtils.getPrefix(path));//F:\

问题:在IE中获取到的是文件的绝对路径,而在W3C浏览器中获取到的是文件的简单名称,如果使用了文件的绝对路径,在文件保存的时候就会有路径找不到的问题。

解决方案FilenameUtils.getName()方法来获取文件的名称

String fileName = FilenameUtils.getName(item.getName());

问题:文件名称相同会发生文件覆盖的问题:

解决方案:使用UUID的值来替代文件的名称

fileName = UUID.randomUUID().toString()+ "." + FilenameUtils.getExtension(fileName);

问题:上传的到文件应该放在项目能访问的位置,为方便能访问到上传的文件,这些文件应该放在项目中

解决方案:在项目中专门使用一个文件夹来存上传的文件 ,在web根路径下建一个upload目录

String dir = req.getServletContext().getRealPath("upload"); 
item.write(new File(dir, fileName));

文件上传是有缓存大小的限制:数据的大小符合则存放在内存,不符合规则不会存放在内容中

文件上传过程中的临时目录:超过缓存大小的文件在上传是会存放在临时目录中, 该目录位于tomcat根/temp的文件夹中(临时目录是可以改的,但是不建议改)

该工厂的默认缓存大小是10KB,默认超过10KB就存放在临时目录

/**
     * The default threshold above which uploads will be stored on disk.
     */
public static final int DEFAULT_SIZE_THRESHOLD = 10240;
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setRepository(repository) //设置临时目录
factory.setSizeThreshold(20*1024) //设置缓存大小

上传文件大小约束

  • 情况1:单个文件超过指定的大小.

  • 情况2:该次请求的全部数据超过指定的大小.

DiskFileItemFactory factory = new DiskFileItemFactory(); 
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setFileSizeMax(3*1024); //设置单个文件不超过3KB
upload.setSizeMax(5*1024);	//设置请求内容大小不能超过5KB
image-20210418195212291

2、文件下载

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>下载</title>
</head>
<body>
<div style="text-align:center;">

    <h3>下载资源</h3>
    <a href="/download?fileName=0f5881d7-c732-4b98-8f6c-9a22b4345d76.jpg">下载</a>
</div>
</body>
</html>
@WebServlet("/download")
public class DownLoadServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 1、接受请求参数
        String fileName = req.getParameter("fileName");
        //解决中文乱码问题
        fileName = new String(fileName.getBytes("ISO-8859-1"),"utf-8");
        //2、找到资源的绝对路径
        String path = getServletContext().getRealPath("/upload/"+fileName);
        //设置文件下载使用的报头域
        resp.setContentType("application/x-msdownload");
        String str = "attachment;filename="+fileName;
        resp.setHeader("Content-Disposition", str);
//		3、磁盘->内存->浏览器
        Files.copy(Paths.get(path), resp.getOutputStream());
    }
}

《Servlet JSP深入详解》 ——孙鑫

JavaWeb——Servlet(全网最详细教程包括Servlet源码分析)

Java Servlet API中文说明文档

易百教程–Servle教程

Servlet&JSP的那些事儿

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值