JavaWeb

目录

1、Tomcat的配置与使用

1.1 Tomcat的简介

讲到Tomcat的使用,我就想起了我刚刚学习JavaWeb时的场景:

老师:这节课我们来讲一下JavaWeb中的服务器昂,它的名字叫Tomcat……..(然后我就发呆去了)

我:没办法呀!下课只能去问大佬喽。

我:大佬大佬,刚刚老师上课讲的服务器Tomcat是什么呀?怎么用呀?

大佬:Tomcat是一个服务器,它可以用来处理客户端的请求,并且将请求处理返回给客户端,巴拉巴拉。

我:

image

最后没办法呀!只能去问度娘了喽。


所以Tomcat就是一个服务器,而且是本地的服务器,可以将我们的Java程序放在Tomcat服务器中,就可以用它来完成不同客户端的请求和响应。当Tomcat启动后,客户端发送请求过来,通过在Tomcat上的Java程序完成请求,然后将处理的结果返回给客户端。

image

官方一点的介绍(百度百科里面的):

Tomcat是Apache 软件基金会(Apache Software Foundation)的Jakarta 项目中的一个核心项目,由Apache、Sun 和其他一些公司及个人共同开发而成。

Tomcat 服务器是一个免费的开放源代码的Web应用服务器(也是Servlet容器),属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP 程序的首选。对于一个初学者来说,可以这样认为,当在一台机器上配置好Apache 服务器,可利用它响应HTML(标准通用标记语言下的一个应用)页面的访问请求。实际上Tomcat是Apache 服务器的扩展,但运行时它是独立运行的,所以当你运行tomcat 时,它实际上作为一个与Apache 独立的进程单独运行的。

1.2 Tomcat的下载

Tomcat的下载地址:http://tomcat.apache.org/

image

可以发现Tomcat已经升级到Tomcat10的内测版本了,但是我们不需要那么新的,可以选择Tomcat8或者Tomcat9的版本就行。那我这里就选择Tomcat8版本吧,直接点击进去。

image

然后根据自己的电脑下载多少位的文件,相信大家应该都是64位的系统吧。

下载之后解压即可使用了。解压后的目录如下:

image

下面我们来详细的介绍各个文件夹的作用。

  1. bin目录:主要是用来存放tomcat的可执行命令,比如启动和停止。命令主要有两大类:以.bat结尾的是windows命令、以.sh结尾的是linux命令。很多环境变量的设置都在此处,例如可以设置JDK路径、TOMCAT路径,startup 用来启动tomcat,shutdown 用来关闭tomcat,修改catalina可以设置tomcat的内存。
  2. conf目录:主要是用来存放tomcat的一些配置文件。例如端口设置,用户信息配置等。
  3. lib目录:主要用来存放tomcat运行需要加载的jar包。
  4. logs目录:用来存放tomcat在运行过程中产生的日志文件,非常重要的是在控制台输出的日志。(清空不会对tomcat运行带来影响)
  5. temp目录:用来让用户存放tomcat在运行过程中产生的临时文件。(清空不会对tomcat运行带来影响)
  6. webapps目录:用来存放客户端可以访问的资源,比如Java程序,当tomcat启动时会去加载webapps目录下的应用程序。可以以文件夹、war包、jar包的形式发布应用。【核心目录】
  7. work目录:用来存放tomcat在运行时的编译后文件,例如JSP编译后的文件。清空work目录,然后重启tomcat,可以达到清除缓存的作用。

再介绍一下conf目录中一些关键文件:

  1. server.xml文件:该文件用于配置和server相关的信息,比如tomcat启动的端口号、配置host主机、配置Context。【核心文件】
  2. web.xml文件:部署描述文件,这个web.xml中描述了一些默认的servlet,部署每个webapp时,都会调用这个文件,配置该web应用的默认servlet。
  3. tomcat-users.xml文件:配置tomcat的用户密码与权限。
  4. context.xml:定义web应用的默认行为。

到此为止对Tomcat的基本介绍都已经讲完了。那它的使用也是非常简单,所以接下来就来启动Tomcat吧。

注意:在启动tomcat之前,你需要安装好JDK,并且配置好环境变量(这个不会去百度)。然后双击bin目录下的startup.bat(如果一闪而过那就是你存放的路径有问题,多半是因为中文路径导致的)。启动动之后如下图:

image

发现有乱码,此时我们修改tomcat的conf目录下的logging.properties中的参数。

将 java.util.logging.ConsoleHandler.encoding = UTF-8 中的UTF-8改到GBK就行了保存后重启tomcat就正常了。

image

注意别修改其它行的编码。

然后在网页中输入localhost:8080就可以出现页面了。

image

这只是运行了一个空的Tomcat,如果需要Java程序,我只需要将项目打成War包然后拷贝到webapps目录下,然后启动Tomcat即可。

image

启动Tomcat服务,注意这里的访问地址有所改变,需要在后面加一个项目名:http://localhost:8080/HelloTomcat/

image

而我们开发软件经常会用到开发工具如Eclipse、IDEA等,所以接下来介绍一下如何在开发工具上使用Tomcat。

1.3 在IDEA上使用Tomcat

在Eclipse中的使用就不说了,因为现在Eclipse大家用的越来越少。直接上IDEA吧。

打开IDEA创建一个HelloTomcat项目。

image

Next之后,输入项目名称就创建完成了。那接下配置Tomcat。

点击IDEA中的Add Configuration 或者打开菜单Run --> 选择Edit Configuration 都可以。

弹出窗口后点击+,然后往下滑,然后选择Tomcat Server的Local(如果没有Tomcat Server则点击 items more)。

image

点击Local之后会出现如下界面,然后将我们下载的Tomcat配置进来:

image

注意:不同的IDEA版本界面可能不同,但是大致操作都是一样的。(我的IDEA2018款,我觉得没必要追求最新版本,适合自己的才是最好的)

然后就是在Tomcat中部署并运行项目,当我们配置完Tomcat后,IDEA下面会弹出一个界面。

右击Tomcat选择Artifacts,然后将项目添加进去,在右击Run就可以了。

image

或者在Edit Configurations中,在建立的Tomcat容器中选择Deployment ,点击右边的“+”号然后,选择选择Artifact。

image

然后在下面右击Tomcat运行,就可以在浏览器中查看运行结果了。


IDEA中的Tomcat热部署:

热部署就是应用正在运行的状态下,修改了它的一些源码后,可以在不重启服务器的情况自动把增量内容编译并部署到服务器上,使得修改立即生效。热部署为了解决的问题有两个, 一是在开发的时候,修改代码后不需要重启应用就能看到效果,大大提升开发效率;二是生产上运行的程序,可以在不停止运行的情况下进行升级,不影响用户使用。

要在IDEA中配置Tomcat热部署非常的简单,但前提是有项目在Tomcat中(否则会报404错误)。

在配置Tomcat中Server的配置里,有个on update actionon frame deactivation

  • on update action
    

    :表示在当项目启动之后,项目代码更新之后的动作是什么?具体如下:

    • Update resources:只更新资源文件,例如更改了jsp,html等。
    • Update classes and resources:更新类信息和资源文件,但是一般更新类信息都是无效,所以如果更改了类,都会重新部署。
    • Redeploy:重新部署项目(推荐)。
    • Restart server:重新启动服务,尽量不要重新启动服务,因为很慢。

image

  • on frame deactivation
    

    :表示当焦点离开IDEA之后会触发哪个更新动作。有三个可以选项:

    • Do nothing:不做任何事。
    • Update resources:只更新资源文件.
    • Update classes and resources:更新类信息和资源文件。

建议设置:

on update action 设置为:Redeploy

on frame deactivation设置为:Update classes and resources

image

配置完后点击Apply即可启动你的tomcat,然后改一下jsp、java文件实验热部署配置是否成功。

注意:IDEA热部署并非绝对实时, 还是会有延时的,假如你手速快的话可能会出现改了并没有马上生效。所以此时不要怀疑热部署没有配置成功,稍微等一会你会看到开发工具左下角会有一个class reload的提示,出现这个提示才说明已经热部。而且如果XML文件有改动的话是不会自动部署的。

1.4 Tomcat的Server结构

在Tomcat中比较核心的一个文件就是conf目录下的server.xml文件,它主要包含Tomcat一些核心配置文件,如:启动端口,用户配置,Context等,Tomcat的启动首先就是加载这个文件。

我们打开server.xml看上去很复杂。其实大部分都是注释。下面是一个简图说明了各组件之间的关系!

image

Tomcat包含的主要组件:服务器Server,服务Service,连接器Connector、引擎Engine、主机Host、上下文Context等。

Server.xml源文件:

<?xml version="1.0" encoding="UTF-8"?>
<!--
  Licensed to the Apache Software Foundation (ASF) under one or more
  contributor license agreements.  See the NOTICE file distributed with
  this work for additional information regarding copyright ownership.
  The ASF licenses this file to You under the Apache License, Version 2.0
  (the "License"); you may not use this file except in compliance with
  the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
-->
<!-- Note:  A "Server" is not itself a "Container", so you may not
     define subcomponents such as "Valves" at this level.
     Documentation at /docs/config/server.html
 -->
<Server port="8005" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <!-- Security listener. Documentation at /docs/config/listeners.html
  <Listener className="org.apache.catalina.security.SecurityListener" />
  -->
  <!--APR library loader. Documentation at /docs/apr.html -->
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  <!-- Prevent memory leaks due to use of particular java/javax APIs-->
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

  <!-- Global JNDI resources
       Documentation at /docs/jndi-resources-howto.html
  -->
  <GlobalNamingResources>
    <!-- Editable user database that can also be used by
         UserDatabaseRealm to authenticate users
    -->
    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
  </GlobalNamingResources>

  <!-- A "Service" is a collection of one or more "Connectors" that share
       a single "Container" Note:  A "Service" is not itself a "Container",
       so you may not define subcomponents such as "Valves" at this level.
       Documentation at /docs/config/service.html
   -->
  <Service name="Catalina">

    <!--The connectors can use a shared executor, you can define one or more named thread pools-->
    <!--
    <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
        maxThreads="150" minSpareThreads="4"/>
    -->


    <!-- A "Connector" represents an endpoint by which requests are received
         and responses are returned. Documentation at :
         Java HTTP Connector: /docs/config/http.html
         Java AJP  Connector: /docs/config/ajp.html
         APR (HTTP/AJP) Connector: /docs/apr.html
         Define a non-SSL/TLS HTTP/1.1 Connector on port 8080
    -->
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    <!-- A "Connector" using the shared thread pool-->
    <!--
    <Connector executor="tomcatThreadPool"
               port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    -->
    <!-- Define an SSL/TLS HTTP/1.1 Connector on port 8443
         This connector uses the NIO implementation. The default
         SSLImplementation will depend on the presence of the APR/native
         library and the useOpenSSL attribute of the
         AprLifecycleListener.
         Either JSSE or OpenSSL style configuration may be used regardless of
         the SSLImplementation selected. JSSE style configuration is used below.
    -->
    <!--
    <Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
               maxThreads="150" SSLEnabled="true">
        <SSLHostConfig>
            <Certificate certificateKeystoreFile="conf/localhost-rsa.jks"
                         type="RSA" />
        </SSLHostConfig>
    </Connector>
    -->
    <!-- Define an SSL/TLS HTTP/1.1 Connector on port 8443 with HTTP/2
         This connector uses the APR/native implementation which always uses
         OpenSSL for TLS.
         Either JSSE or OpenSSL style configuration may be used. OpenSSL style
         configuration is used below.
    -->
    <!--
    <Connector port="8443" protocol="org.apache.coyote.http11.Http11AprProtocol"
               maxThreads="150" SSLEnabled="true" >
        <UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" />
        <SSLHostConfig>
            <Certificate certificateKeyFile="conf/localhost-rsa-key.pem"
                         certificateFile="conf/localhost-rsa-cert.pem"
                         certificateChainFile="conf/localhost-rsa-chain.pem"
                         type="RSA" />
        </SSLHostConfig>
    </Connector>
    -->

    <!-- Define an AJP 1.3 Connector on port 8009 -->
    <!--
    <Connector protocol="AJP/1.3"
               address="::1"
               port="8009"
               redirectPort="8443" />
    -->

    <!-- An Engine represents the entry point (within Catalina) that processes
         every request.  The Engine implementation for Tomcat stand alone
         analyzes the HTTP headers included with the request, and passes them
         on to the appropriate Host (virtual host).
         Documentation at /docs/config/engine.html -->

    <!-- You should set jvmRoute to support load-balancing via AJP ie :
    <Engine name="Catalina" defaultHost="localhost" jvmRoute="jvm1">
    -->
    <Engine name="Catalina" defaultHost="localhost">

      <!--For clustering, please take a look at documentation at:
          /docs/cluster-howto.html  (simple how to)
          /docs/config/cluster.html (reference documentation) -->
      <!--
      <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>
      -->

      <!-- Use the LockOutRealm to prevent attempts to guess user passwords
           via a brute-force attack -->
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <!-- This Realm uses the UserDatabase configured in the global JNDI
             resources under the key "UserDatabase".  Any edits
             that are performed against this UserDatabase are immediately
             available for use by the Realm.  -->
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>

      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">

        <!-- SingleSignOn valve, share authentication between web applications
             Documentation at: /docs/config/valve.html -->
        <!--
        <Valve className="org.apache.catalina.authenticator.SingleSignOn" />
        -->

        <!-- Access log processes all example.
             Documentation at: /docs/config/valve.html
             Note: The pattern used is equivalent to using pattern="common" -->
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />

      </Host>
    </Engine>
  </Service>
</Server>

下面简单介绍几个常用组件:

①、Server组件

如上面示例文件中定义的:

<Server port=”8005” shutdown=”SHUTDOWN”>

这会让Tomcat启动一个server实例(即一个JVM),它监听在8005端口以接收shutdown命令。各Server的定义不能使用同一个端口,这意味着如果在同一个物理机上启动了多个Server实例,必须配置它们使用不同的端口。这个端口的定义用于为管理员提供一个关闭此实例的便捷途径,因此,管理员可以直接telnet至此端口使用SHUTDOWN命令关闭此实例。不过,基于安全角度的考虑,这通常不允许远程进行。

Server的相关属性:

  • className: 用于实现此Server容器的完全限定类的名称,默认为org.apache.catalina.core.StandardServer;
  • port: 接收shutdown指令的端口,默认仅允许通过本机访问,默认为8005;
  • shutdown:发往此Server用于实现关闭tomcat实例的命令字符串,默认为SHUTDOWN;

②、Service组件

Service主要用于关联一个引擎和与此引擎相关的连接器,每个连接器通过一个特定的端口和协议接收入站请求交将其转发至关联的引擎进行处理。因此,Service要包含一个引擎、一个或多个连接器。

如上面示例中的定义:

<Service name=”Catalina”>

这定义了一个名为Catalina的Service,此名字也会在产生相关的日志信息时记录在日志文件当中。

Service相关的属性:

  • className: 用于实现service的类名,一般都是org.apache.catalina.core.StandardService。
  • name:此服务的名称,默认为Catalina;

③、Connector组件

进入Tomcat的请求可以根据Tomcat的工作模式分为如下两类:

  • Tomcat作为应用程序服务器:请求来自于前端的web服务器,这可能是Apache, IIS, Nginx等;
  • Tomcat作为独立服务器:请求来自于web浏览器;

Tomcat应该考虑工作情形并为相应情形下的请求分别定义好需要的连接器才能正确接收来自于客户端的请求。一个引擎可以有一个或多个连接器,以适应多种请求方式。

定义连接器可以使用多种属性,有些属性也只适用于某特定的连接器类型。一般说来,常见于server.xml中的连接器类型通常有4种:

  • HTTP连接器
  • SSL连接器
  • AJP 1.3连接器
  • proxy连接器

如上面示例server.xml中定义的HTTP连接器:

<Connector port="8080" protocol="HTTP/1.1" 
  maxThreads="150" connectionTimeout="20000" 
  redirectPort="8443"/>

定义连接器时可以配置的属性非常多,但通常定义HTTP连接器时必须定义的属性只有“port”,定义AJP连接器时必须定义的属性只有"protocol",因为默认的协议为HTTP。以下为常用属性的说明:

  • address:指定连接器监听的地址,默认为所有地址,即0.0.0.0;
  • maxThreads:支持的最大并发连接数,默认为200;
  • port:监听的端口,默认为0;
  • protocol:连接器使用的协议,默认为HTTP/1.1,定义AJP协议时通常为AJP/1.3;
  • redirectPort:如果某连接器支持的协议是HTTP,当接收客户端发来的HTTPS请求时,则转发至此属性定义的端口;
  • connectionTimeout:等待客户端发送请求的超时时间,单位为毫秒,默认为60000,即1分钟;
  • enableLookups:是否通过request.getRemoteHost()进行DNS查询以获取客户端的主机名;默认为true;
  • acceptCount:设置等待队列的最大长度;通常在tomcat所有处理线程均处于繁忙状态时,新发来的请求将被放置于等待队列中;

下面是一个定义了多个属性的SSL连接器:

<Connector port="8443" 
 maxThreads="150" minSpareThreads="25" maxSpareThreads="75" 
 enableLookups="false" acceptCount="100" debug="0" scheme="https" secure="true" 
 clientAuth="false" sslProtocol="TLS" />

④、Engine组件

Engine是Servlet处理器的一个实例,即servlet引擎,默认为定义在server.xml中的Catalina。Engine需要defaultHost属性来为其定义一个接收所有发往非明确定义虚拟主机的请求的host组件。如前面示例中定义的:

<Engine name="Catalina" defaultHost="localhost">

常用的属性定义:

  • defaultHost:Tomcat支持基于FQDN的虚拟主机,这些虚拟主机可以通过在Engine容器中定义多个不同的Host组件来实现;但如果此引擎的连接器收到一个发往非非明确定义虚拟主机的请求时则需要将此请求发往一个默认的虚拟主机进行处理,因此,在Engine中定义的多个虚拟主机的主机名称中至少要有一个跟defaultHost定义的主机名称同名;
  • name:Engine组件的名称,用于日志和错误信息记录时区别不同的引擎;

注:Engine容器中可以包含Realm、Host、Listener和Valve子容器。

⑤、Host组件

位于Engine容器中用于接收请求并进行相应处理的主机或虚拟主机,如前面默认配置文件中定义:

<Host name="localhost" appBase="webapps" 
unpackWARs="true" autoDeploy="true" 
xmlValidation="false" xmlNamespaceAware="false"> 
</Host>

常用属性说明:

  • appBase:此Host的webapps目录,即存放非归档的web应用程序的目录或归档后的WAR文件的目录路径;可以使用基于$CATALINA_HOME的相对路径;
  • autoDeploy:在Tomcat处于运行状态时放置于appBase目录中的应用程序文件是否自动进行deploy;默认为true;
  • unpackWars:在启用此webapps时是否对WAR格式的归档文件先进行展开;默认为true;

下面是虚拟主机定义示例:

<Engine name="Catalina" defaultHost="localhost">
 <Host name="localhost" appBase="webapps">
 <Context path="" docBase="ROOT"/>
 <Context path="/bbs" docBase="/web/bss"
  reloadable="true" crossContext="true"/>
 </Host>
 <Host name="mail.test.com" appBase="/web/mail">
 <Context path="" docBase="ROOT"/>
 </Host>
</Engine>

主机别名定义:

如果一个主机有两个或两个以上的主机名,额外的名称均可以以别名的形式进行定义,如下:

<Host name="www.test.com" appBase="webapps" unpackWARs="true">
 <Alias>test.com</Alias>
</Host>

⑥、Context组件

Context组件是最内层次的组件,它表示Web应用程序本身。配置一个Context最主要的是指定Web应用程序的根目录,以便Servlet容器能够将用户请求发往正确的位置。Context组件也可包含自定义的错误页,以实现在用户访问发生错误时提供友好的提示信息。Context在某些意义上类似于apache中的路径别名,一个Context定义用于标识tomcat实例中的一个Web应用程序;如下面的定义:

<!-- Tomcat Root Context -->
<Context path="" docBase="/web/webapps"/>
<!-- buzzin webapp -->
<Context path="/bbs"
 docBase="/web/threads/bbs"
 reloadable="true">
</Context>
<!-- chat server -->
 <Context path="/chat" docBase="/web/chat"/>
<!-- darian web -->
<Context path="/darian" docBase="darian"/>

在Tomcat中,每一个context定义也可以使用一个单独的XML文件进行,其文件的目录为$CATALINA_HOME/conf//。可以用于Context中的XML元素有Loader,Manager,Realm,Resources和WatchedResource。

常用的属性定义有:

  • docBase:相应的Web应用程序的存放位置;也可以使用相对路径,起始路径为此Context所属Host中appBase定义的路径;切记,docBase的路径名不能与相应的Host中appBase中定义的路径名有包含关系,比如,如果appBase为deploy,而docBase绝不能为deploy-bbs类的名字;
  • path:相对于Web服务器根路径而言的URI;如果为空“”,则表示为此webapp的根路径;如果context定义在一个单独的xml文件中,此属性不需要定义;
  • reloadable:是否允许重新加载此context相关的Web应用程序的类;默认为false;

⑦、Realm组件

一个Realm表示一个安全上下文,它是一个授权访问某个给定Context的用户列表和某用户所允许切换的角色相关定义的列表。因此,Realm就像是一个用户和组相关的数据库。定义Realm时惟一必须要提供的属性是classname,它是Realm的多个不同实现,用于表示此Realm认证的用户及角色等认证信息的存放位置。

  • JAASRealm:基于Java Authintication and Authorization Service实现用户认证;
  • JDBCRealm:通过JDBC访问某关系型数据库表实现用户认证;
  • JNDIRealm:基于JNDI使用目录服务实现认证信息的获取;
  • MemoryRealm:查找tomcat-user.xml文件实现用户信息的获取;
  • UserDatabaseRealm:基于UserDatabase文件(通常是tomcat-user.xml)实现用户认证,它实现是一个完全可更新和持久有效的MemoryRealm,因此能够跟标准的MemoryRealm兼容;它通过JNDI实现;

下面是一个常见的使用UserDatabase的配置:

<Realm className=”org.apache.catalina.realm.UserDatabaseRealm”
 resourceName=”UserDatabase”/>
下面是一个使用JDBC方式获取用户认证信息的配置:
 <Realm className="org.apache.catalina.realm.JDBCRealm" debug="99"
 driverName="org.gjt.mm.mysql.Driver"
 connectionURL="jdbc:mysql://localhost/authority"
 connectionName="test" connectionPassword="test"
 userTable="users" userNameCol="user_name"
 userCredCol="user_pass"
 userRoleTable="user_roles" roleNameCol="role_name" />

⑧、Valve组件

Valve类似于过滤器,它可以工作于Engine和Host/Context之间、Host和Context之间以及Context和Web应用程序的某资源之间。一个容器内可以建立多个Valve,而且Valve定义的次序也决定了它们生效的次序。Tomcat中实现了多种不同的Valve:

  • AccessLogValve:访问日志Valve
  • ExtendedAccessValve:扩展功能的访问日志Valve
  • JDBCAccessLogValve:通过JDBC将访问日志信息发送到数据库中;
  • RequestDumperValve:请求转储Valve;
  • RemoteAddrValve:基于远程地址的访问控制;
  • RemoteHostValve:基于远程主机名称的访问控制;
  • SemaphoreValve:用于控制Tomcat主机上任何容器上的并发访问数量;
  • JvmRouteBinderValve:在配置多个Tomcat为以Apache通过mod_proxy或mod_jk作为前端的集群架构中,当期望停止某节点时,可以通过此Valve将用记请求定向至备用节点;使用此Valve,必须使用JvmRouteSessionIDBinderListener;
  • ReplicationValve:专用于Tomcat集群架构中,可以在某个请求的session信息发生更改时触发session数据在各节点间进行复制;
  • SingleSignOn:将两个或多个需要对用户进行认证webapp在认证用户时连接在一起,即一次认证即可访问所有连接在一起的webapp;
  • ClusterSingleSingOn:对SingleSignOn的扩展,专用于Tomcat集群当中,需要结合ClusterSingleSignOnListener进行工作;

RemoteHostValve和RemoteAddrValve可以分别用来实现基于主机名称和基于IP地址的访问控制,控制本身可以通过allow或deny来进行定义,这有点类似于Apache的访问控制功能;如下面的Valve则实现了仅允许本机访问/probe:

<Context path="/probe" docBase="probe">
<Valve className="org.apache.catalina.valves.RemoteAddrValve"
allow="127\.0\.0\.1"/>
</Context>

其中相关属性定义有:

  • className:相关的java实现的类名,相应于分别应该为org.apache.catalina.valves.RemoteHostValve或org.apache.catalina.valves.RemoteAddrValve;
  • allow:以逗号分开的允许访问的IP地址列表,支持正则表达式,因此,点号“.”用于IP地址时需要转义;仅定义allow项时,非明确allow的地址均被deny;
  • deny: 以逗号分开的禁止访问的IP地址列表,支持正则表达式;使用方式同allow;

Tomcat Server处理一个HTTP请求的过程:

  1. 用户点击网页内容,请求被发送到本机端口8080,被在那里监听的Coyote HTTP/1.1 Connector获得。
  2. Connector把该请求交给它所在的Service的Engine来处理,并等待Engine的回应。
  3. Engine获得请求localhost/test/index.jsp,匹配所有的虚拟主机Host。
  4. Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机),名为localhost的Host获得请求/test/index.jsp,匹配它所拥有的所有的Context。Host匹配到路径为/test的Context(如果匹配不到就把该请求交给路径名为“ ”的Context去处理)。
  5. path=“/test”的Context获得请求/index.jsp,在它的mapping table中寻找出对应的Servlet。Context匹配到URL PATTERN为*.jsp的Servlet,对应于JspServlet类。
  6. 构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet()或doPost().执行业务逻辑、数据存储等程序。
  7. Context把执行完之后的HttpServletResponse对象返回给Host。
  8. Host把HttpServletResponse对象返回给Engine。
  9. Engine把HttpServletResponse对象返回Connector。
  10. Connector把HttpServletResponse对象返回给客户Browser。

参考资料:

https://blog.51cto.com/freeloda/1299644

https://blog.csdn.net/u014231646/article/details/79482195

2、IDEA创建一个JavaWeb项目

2.1 创建项目

①、打开IDEA

image

②、项目名称

image

点击Finish后项目就创建成功了,如下图:

image

接下来再在web中的WEB-INF下创建两个文件夹:classes和lib(其中classes存放编译后输出的class文件,lib用于存放jar包)

③、配置classes字节码文件路径

我们点击操作栏的image图标,或者File—>Project Structure (快捷键Ctrl+Alt+Shift+S),然后找到Module。

image

创建一个classes的文件,然后再设置它的存放路径,还是这个界面。

image

设置完之后classes目录会变成橙色。

④、配置jar包存放路径(lib文件夹),

我们为什么要配置jar包的存放路径:是因为直接将jar包拷贝到创建好的bin目录中,程序在运行的时候是不会加载到这些包的,所以我们需要设置一下。

首先在WEB-INF创建一个lib目录,创建方法和创建classes一样。

image

image

image

然后OK就可以了

image

然后到此为止一个JavaWeb项目就全部配置完成了。

2.2 配置项目

配置项目也就是将JavaWeb项目配置Tomcat服务器中,之前写过一篇Tomcat的配置与使用,这里和那里差不多,所以就把那里的图片拷贝过来了。

点击IDEA中的Add Configuration,或者打开菜单Run --> 选择Edit Configuration 都可以。

弹出窗口后点击+,然后往下滑,然后选择Tomcat Server的Local(如果没有Tomcat Server则点击 items more)。

image

点击Local之后会出现如下界面:

image

注意:不同的IDEA版本界面可能不同,但是大致操作都是一样的。(我的IDEA2018款,我觉得没必要追求最新版本,适合自己的才是最好的)

然后就是在Tomcat中部署并运行项目,当我们配置完Tomcat后,IDEA下面会弹出一个界面。

右击Tomcat选择Artifacts,然后将项目添加进去,在右击Run就可以了。

image

image

或者在Edit Configurations中,在建立的Tomcat容器中选择Deployment ,点击右边的“+”号然后,选择选择Artifact。

image

然后在下面右击Tomcat运行,就可以在浏览器中查看运行结果了。

2.3 运行项目

我们修改一下index.jsp页面内容然后启动Tomcat。

image

运行之后页面会自动弹出,如果没有弹出就手动输入localhost:8080即可。

image

3、servlet

3.1 本章前言

对呀!现在都2020年了,时间过得真快,本文来说说现在Servlet它还有必要学吗?因为Servlet已经是一个非常非常古老的技术了,而且在实际开发中几乎不会用到,在面试中也几乎不会问到Servlet相关的知识。所以我们不需要学习Servlet了吗?这样想就大错特错了。我们后面会学习到Struts2和SpringMVC框架,它两的底层都是跟Servlet有关,所以Servlet还是很有必要的学习的,最好不要跳过它。我们只有打下坚实的基础,后面的框架学习起来才能得心应手。

3.2 什么是Servlet

Servlet(Server Applet)是Java Servlet的简称,称为小服务程序或服务连接器,用Java编写的服务器端程序,主要功能在于交互式地浏览和修改数据,生成动态Web内容。这是百度百科上的一段话。说简单点Servlet就是对客户端发送过来的请求进行处理,并且作出相应的响应,其本质就是一个实现了Servlet接口的实现类。

您可能还听说过 Applet,它和 Servlet 是相对的:

  • Java Servlet 是“服务器端小程序”,运行在服务器上,用来开发动态网站;
  • Java Applet 是“客户端小程序”,一般被嵌入到 HTML 页面,运行在支持 Java 的浏览器中。

Applet 和 Servlet 都是基于 Java 的一种技术,功能都非常强大,但是 Applet 开发步骤繁杂,而且只能在安装 Java 虚拟机(JVM)的计算机上运行,现在已经被 JavaScript 全面替代,几乎没有人再学习 Applet。

其过程如下:

  1. 客户端发送请求至Web服务器端。
  2. 服务器将请求信息发送至Servlet。
  3. Servlet 生成响应内容并将其传给服务器。响应内容动态生成,通常取决于客户端的请求。
  4. 服务器将响应返回给客户端。

image

注:servlet程序是由servlet容器(即tomcat服务器)进行管理,包括实例化、初始化、服务、销毁的过程都由tomca在指定时间内完成。

服务器的三大组件:

  • servlet:用于处理请求和响应
  • filter:用于过滤请求和响应
  • listener:用于监听服务器的状态

3.3 创建Servlet程序

在创建Servlet之前需要提前配置好环境:1、安装好JDK;2、开发工具Eclipse或IDEA(推荐);3、安装Tomcat。这三个条件是必须的,具体怎么配置网上教程很多,这里不多BB。

创建Servlet程序的流程如下:

  • 编写一个Java类,然后继承HttpServlet(或者继承GenericServlet,又或者直接实现Servlet接口)。
/**
 * @author tanghaorong
 * @date 2020-04-20
 * @desc 创建一个Servlet程序
 */
public class MyServlet extends HttpServlet {

}

注意:我们一般都是继承HTTPServlet,因为HttpServlet是指能够处理HTTP协议请求的Servlet,它在原有Servlet接口上添加了一些与HTTP协议处理方法,它比Servlet接口的功能更为强大。因此开发人员在编写Servlet时,通常应继承这个类,而避免直接去实现Servlet接口和继承GenericServlet。而且HttpServlet在实现Servlet接口时,覆写了service方法,该方法体内的代码会自动判断用户的请求方式,如为GET请求,则调用HttpServlet的doGet方法,如为Post请求,则调用doPost方法。因此,开发人员在编写Servlet时,通常只需要覆写doGet或doPost方法,而不要去覆写service方法

  • 重写HttpServlet类中的doGet和doPost方法(IDEA快捷键Ctrl+O)。
/**
 * @author tanghaorong
 * @date 2020-04-20
 * @desc 重写父类的doGet、doPost方法
 */
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("第一个Servlet程序");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }
}
  • 使用web.xml文件或者注解对servlet进行配置。推荐使用注解

前面两步没什么可说的,重点是在配置web.xml文件上,这一步决定了我们的请求和响应是哪个Servlet来完成的。上面说到配置Servlet有两种方式:一种是使用web.xml文件配置,另外一种就是使用注解配置,所以下面我们来详解介绍这两种配置方式:

3.3.1 使用web.xml文件配置

我们打开WEB-INF/web.xml文件,在<web-app>元素中编写一个<servlet>元素用于配置一个Servlet,它包含有两个主要的子元素:<servlet-name><servlet-class>,分别用于设置Servlet的名称和Servlet的完整类名。另外一个<servlet-mapping>元素用于映射一个已注册的Servlet的一个对外访问路径,它包含有两个子元素:<servlet-name><url-pattern>,分别用于指定映射到哪个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>
        <!--Servlet名称,最好见名知意-->
        <servlet-name>HelloServlet</servlet-name>
        <!--Servlet的全类名,即包+类-->
        <servlet-class>com.thr.MyServlet</servlet-class>
    </servlet>

    <!--配置Servlet的映射-->
    <servlet-mapping>
        <!--映射到哪个Servlet,注意一点要与上面有的名称一样-->
        <servlet-name>HelloServlet</servlet-name>
        <!-- Servlet的对外访问路径 -->
        <url-pattern>/HelloServlet</url-pattern>
    </servlet-mapping>

</web-app>

完成上面的web.xml配置后,当服务器运行之后,Servlet程序就可以被外界访问了,打开浏览器访问如下地址http://localhost:8080/HelloServlet

注:如果访问不了则在访问地址加上项目名:http://localhost:8080/{项目名称}/HelloServlet

然后查看可知打印数据:

image

补充:同一个Servlet可以被映射到多个URL上,即多个<servlet-mapping>元素的<servlet-name>子元素的设置值可以是同一个Servlet的名称。 例如:

<!--配置一个Servlet-->
<servlet>
    <!--Servlet名称,最好见名知意-->
    <servlet-name>HelloServlet</servlet-name>
    <!--Servlet的全类名,即包+类-->
    <servlet-class>com.thr.MyServlet</servlet-class>
</servlet>

<!--配置Servlet的映射-->
<servlet-mapping>
    <!--映射到哪个Servlet,注意一点要与上面有的名称一样-->
    <servlet-name>HelloServlet</servlet-name>
    <!-- Servlet的对外访问路径 -->
    <url-pattern>/HelloServlet</url-pattern>
</servlet-mapping>


<servlet-mapping>
    <servlet-name>HelloServlet</servlet-name>
    <url-pattern>/HelloServlet/HelloServlet</url-pattern>
</servlet-mapping>

<servlet-mapping>
    <servlet-name>HelloServlet</servlet-name>
    <url-pattern>/HelloServlet/HelloServlet/HelloServlet</url-pattern>
</servlet-mapping>

通过上面的配置,当我们想访问名称是MyServlet的Servlet,可以使用如下的几个地址去访问,但结果都是访问的同一个Servlet:

  • http://localhost:8080/HelloServlet
  • http://localhost:8080/HelloServlet/HelloServlet
  • http://localhost:8080/HelloServlet/HelloServlet/HelloServlet

<init-param>初始化参数

<init-param>
    <param-name>abc</param-name>
    <param-value>123</param-value>
</init-param>

一个Servlet可以配置一个或多个初始化参数。

读取初始化param对应的参数:①可以使用Servlet的getInitParameter(String param);②也可以由ServletConfig对象获取

  • 在应用程序中,可以使用Servlet的getInitParameter(String param)来读取初始化param对应的参数;若要读取所有的初始化参数名称,则可以使用getInitParameterNames()方法获得所有的参数名称,类型为枚举(Enumeration)。
//获取所有初始化参数
Enumeration<String> strs=this.getInitParameterNames();
while(strs.hasMoreElements()) {
    str=strs.nextElement();
    System.out.println(str+"     "+this.getInitParameter(str));
    //-->abc 123
    //-->aaa 111
}
  • 这些初始化参数也可以由ServletConfig对象获取,Servlet提供的getServletConfig()方法,提供了ServletConfig对象。由ServletConfig获取初始化参数和由Servlet获取初始化参数的方法是一样的。

ServletConfig对象: 主要是用于加载servlet的初始化参数

在一个web应用可以存在多个ServletConfig对象(一个Servlet对应一个ServletConfig对象);

ServletConfig对象的创建和获取:

  • 创建时机: 在创建完servlet对象之后,在调用init方法之前创建。
  • 得到对象: 直接从有参数的init方法中得到

ServletConfig config=this.getServletConfig();

//两种调用getInitParameter的情况,视情况而定
//第一种 有Servlet获取初始化参数
String str=this.getInitParameter("abc");  
//第二种 由Servlet提供的方法getServletConfig()提供了ServletConfig
//在web容器创建Servlet实例对象后,ServletConfig对象就会被创建,且会自动将初始化参数封装到ServletConfig对象,在调用init()方法时,ServletConfig对象将传递给Servlet
ServletConfig config = this.getServletConfig();
String str=config.getInitParameter("abc");

初始化参数的一个有趣应用是进行单个文件的访问加密,原理是将用户名和密码写入初始化参数中,这样的好处是简单、方便,缺点是不灵活,安全性也不高,适用于临时性的措施。

<context-param>上下文参数

<context-param>
    <param-name>root</param-name>
    <param-value>123</param-value>
</context-param>

获取context-param需要使用ServletContext对象。ServletContext对象可以通过在Servlet中的getServletConfig().getServletContext()方法获得。得到ServletContext对象后,使用getInitParameter(String param)方法获取名为param的参数值,通过getInitParameterNames()获取所有的context-param名称。

ServletContext context=this.getServletContext();
String root=context.getInitParameter("root");
System.out.println("root="+root);

3.3.2 使用注解配置(推荐)

注:用了注解,web.xml中就不能再配置该Servlet了

我们都知道使用web.xml文件来配置是很头痛的事情,随着系统的开发,配置文件肯定会越来越多,里面的文件也会看的眼花缭乱。所以Servlet3.0之后提供了注解(annotation),使得不再需要在web.xml文件中进行Servlet的配置,而是使用注解@WebServlet代替了web.xml,从而简化开发流程。

下面是注解@WebServlet源码中的属性列表:

属性名 类型 描述
name String 指定Servlet 的 name 属性,等价于<servlet-name>如果没有显式指定,则该 Servlet 的取值即为类的全限定名。
value String[] 该属性等价于下面urlPatterns属性。这两个属性不能同时使用。
urlPatterns String[] 指定一组Servlet的URL匹配模式,等价于<url-pattern>标签。
loadOnStartup int 指定Servlet的加载顺序,等价于<load-on-startup>标签。
initParams WebInitParam[] 指定一组Servlet初始化参数,等价于<init-param>标签
asyncSupported boolean 声明Servlet是否支持异步操作,等价于<async-supported>标签。
smallIcon String 此Servlet的小图标。
largeIcon String 此Servlet的大图标。
description String 该Servlet的描述信息,等价于<description>标签。
displayName String 该Servlet的显示名,通常配合工具使用,等价于<display-name>标签。

使用注解配置Servlet的示例如下:

package com.thr;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author tanghaorong
 * @date 2020-04-20
 * @desc 使用注解@WebServlet配置Servlet
 */

//name = "MyServlet":servlet名称,相当于web.xml中的<servlet-name>
//urlPatterns = "/HelloServlet":servlet的访问路径,相当于<url-pattern>

@WebServlet(name = "MyServlet",value = "/HelloServlet")
public class MyServlet extends HttpServlet {

    public MyServlet() {
        super();
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException {
        System.out.println("get 请求执行");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException {
        System.out.println("post 请求执行");
    }
}

上面实现的效果和web.xml中是一模一样的,是不是这样太爽了,所以一般推荐使用注解进行开发,现在无论任何系统开发都基本上摒弃了XML开发,因为开发效率不高,而且排错也很麻烦。

使用 ***** 通配符模糊匹配映射Servlet程序,在前面的所有例子中我们映射的URL都是精确匹配,而在Servlet映射到的URL中也是可以使用 * 通配符进行模糊匹配的。

但是只能有两种固定的格式:一种格式是"*.扩展名"(例如:*.do *.action),另一种格式是以正斜杠(/)开头并以"/*"结尾。

它们的匹配规则如下:

  • /*:匹配任何路径映射到servlet。
  • /abc/*:匹配/abc/下的任意路径映射到servlet。
  • /abc/def:只匹配/abc/def路径下的servlet。
  • *.do:匹配 任意名称 .do 的路径映射到servlet。

其中例如:/abc/*.do、/*.do、abc*.do 这些都是非法的,启动时候会报错,我亲自去试了一下,反正 *.后缀名 这种格式前面是不能加正斜杠(/)的

还有要注意的是,可能会出现这样的情况,例如:我请求的URL为:/abc/edf,而这个路径有两个Servlet匹配(/* 和 /abc/*),那么它会选择哪一个呢?

答:会选择/abc/edf 的Servlet,因为匹配的原则是"谁长得更像就找谁"


举一个完整的Servlet栗子:

①、修改index.jsp页面。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>登录</title>
  </head>

  <body>
  <h1 align="center" style="color: red;">欢迎您登录系统</h1><hr/>
  <div align="center">
    <form method="post" action="/login">
      <table>
        <tr>
          <td>Username:</td>
          <td><input type="text" name="username"/></td>
        </tr>

        <tr>
          <td>Password:</td>
          <td><input type="password" name="password"/></td>
        </tr>

        <tr>
          <td></td>
          <td>
            <input type="submit" value="登录"/>
            <input type="reset" value="重置"/>
          </td>
        </tr>
      </table>
    </form>
  </div>
  </body>
</html>

②、创建名为LoginServlet的Servlet类。并用@WebServlet注释。

package com.thr;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @author Administrator
 * @date 2020-04-20
 * @desc Servlet登录的例子
 */
@WebServlet(name = "loginServlet",value = "/login")
public class LoginServlet extends HttpServlet {

    //因为设置了为Post请求,使用doGet方法就不写了
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 设置request的编码
        request.setCharacterEncoding("UTF-8");
        // 获取信息
        String name = request.getParameter("username");
        String password = request.getParameter("password");
        System.out.println(name+":"+password);
        // 设置response的编码
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html");
        // 获取PrintWriter对象
        PrintWriter out = response.getWriter();
        // 输出信息
        out.println("<HTML>");
        out.println("<HEAD><TITLE>登录信息</TITLE></HEAD>");
        out.println("<BODY>");
        out.println("姓名:" + name + "<br>");
        out.println("密码:" + password + "<br>");
        out.println("</BODY>");
        out.println("</HTML>");
        // 释放PrintWriter对象
        out.flush();
        out.close();
    }
}

执行结果:

  • 登录页面:

image

  • 提交登录结果信息:

image

3.4 Servlet生命周期

Servlet接口中定义了五个方法,我们看一看Servlet接口中方法:

package javax.servlet;

import java.io.IOException;

public interface Servlet {
    void init(ServletConfig var1) throws ServletException;

    ServletConfig getServletConfig();

    void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

    String getServletInfo();

    void destroy();
}

其中有三个为生命周期方法:init(),service(),destory():

  1. init()方法用于初始化该Servlet。当Servlet第一次被加载时,Servlet引擎调用这个Servlet的init()方法,而且只调用一次。
  2. service()方法用于处理请求。这是Servlet最重要的方法,是真正处理请求的地方。对于每个请求,Servlet引擎都会调用Servlet的service方法,并把Servlet请求对象和Servlet响应对象最为参数传递给它,并且判断Servlet调用的是doGet方法还是doPost方法。
  3. destory()方法用于销毁该Servlet。这是相对于init的可选方法,当Servlet即将被卸载时由Servlet引擎来调用,这个方法用来清除并释放在init方法中所分配的资源。

此外,还有两个非生命周期方法。

  • getServletInfo()方法用于返回Servlet的一段描述,可以返回一段字符串。
  • getServletConfig()方法用于返回由Servlet容器传给init()方法的ServletConfig对象。

下面来编写一个简单的Servlet来验证一下它的生命周期:

package com.thr;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;

/**
 * @author Administrator
 * @date 2020-04-22
 * @desc Servlet生命周期
 */
@WebServlet(value = "/HelloServlet1")
public class MyServlet1 implements Servlet {
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
        System.out.println("Servlet完成初始化--init()");
    }

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

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("Servlet正在执行操作--service()");
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {
        System.out.println("Servlet已经销毁--destroy()");
    }
}
折叠 

服务器运行后我们在浏览器访问:http://localhost:8080/HelloServlet1,控制台输出了如下信息:

image

然后,我们在浏览器中刷新3遍:

image

接下来,我们关闭Servlet容器:

image

以上就是一个Servlet的整个生命周期了。可以发现,在Servlet的整个生命周期内,Servlet的init()方法只被调用一次。也就是说当客户端多次Servlet请求时,服务器只会创建一个Servlet实例对象,而且Servlet实例对象一旦创建,它就会驻留在内存中,为后续的其它请求服务,直至web容器退出,Servlet实例对象才会销毁。而对一个Servlet的每次访问请求都导致Servlet引擎调用一次servlet的service方法。对于每次访问请求,Servlet引擎都会创建一个新的HttpServletRequest请求对象和一个新的HttpServletResponse响应对象,然后将这两个对象作为参数传递给它调用的Servlet的service()方法,service方法再根据请求方式分别调用doXXX方法。


上面说当客户端在第一次访问Servlet的时候会才创建Servlet实例对象,那如果这个Servlet程序要处理的信息很多,那就会造成第一次访问的Servlet加载时间较长。所以为了解决这样的问题Servlet提供了自动加载机制,就是在启动服务器的时候就将Servlet加载起来,它的操作很简单。我们可以在web.xml中配置也可以在注解中配置。

  • 在web.xml中进行配置:
    <servlet>
        <servlet-name>HelloServlet</servlet-name>
        <servlet-class>com.thr.MyServlet</servlet-class>

        <!-- 让servlet对象自动加载 -->
        <load-on-startup>1</load-on-startup>
    </servlet>

注意:<load-on-startup></load-on-startup>中的整数值越大,创建优先级越低!

  • 在注解中进行配置:
@WebServlet(name = "MyServlet",value = "/HelloServlet",loadOnStartup = 1)
public class MyServlet extends HttpServlet {

通过上面的实例,可以看到Servlet在创建时只会执行一次init()方法,后面每次点击都只调用 service() 方法。那么Servlet的一次执行过程是什么样的呢?

image

上面这幅图可以这样理解:

  1. 客户端向 Web 服务器发送请求,服务器查询 web.xml 文件配置。根据请求信息找到对应的 Servlet。

  2. Servlet 引擎检查是否已经装载并创建了该 Servlet 的实例对象,如果有,则直接执行第4步,否则执行第3步,

  3. Web 服务器加载 Servlet,并调用 Servlet 构造器(只会调用一次),创建 Servlet 的实例对象。并调用 init() 方法,完成 Servlet 实例对象的初始化(只会调用一次)。

  4. Web 服务器把接收到的 http 请求封装成 ServletRequest 对象,并创建一个 响应消息的 ServletResponse 对象,作为 service() 方法的参数传入。(每一次访问都会调用一次该方法)

  5. 执行 service()方法,并将处理信息封装到 ServletResponse 对象中返回

  6. 浏览器拆除 ServletResponse 对象,形成 http 响应格式,返回给客户端。

  7. Web 应用程序停止或者重新启动之前,Servlet 引擎将卸载 Servlet实例,并在卸载之前调用 destory() 方法

3.5 ServletConfig对象

ServletConfig表示一个Servlet的配置信息,每个Servlet对象都有一个封装Servlet配置的ServletConfig对象。因此可通过此对象获取servlet相关信息,也可以用来读取web.xml中用<init-param>配置的Servlet初始化参数。

image

当我们的Servlet配置了初始化参数后,启动服务器,web容器在创建Servlet实例对象后,接着ServletConfig对象就会被创建,而且会自动将初始化参数封装到ServletConfig对象中,并在调用Servlet的init()方法时,将ServletConfig对象传递给创建好的Servlet。进而,我们通过ServletConfig对象就可以得到当前Servlet的初始化参数信息。

image-20220728110358089

ServletConfig中有四个方法如下:

  • String getServletName():获取当前Servlet的名称,即:中的内容。
  • ServletContext getServletContext():获取当前当前Web的应用上下文,即整个Servlet。
  • String getInitParameter(String var1):通过名称获取指定初始化参数的值。
  • Enumeration<String> getInitParameterNames():获取所有初始化参数的名称。

使用ServletConfig对象获取初始化数据的简单举例:

①、在Servlet的配置文件web.xml中,可以使用一个或多个标签为servlet配置一些初始化参数。

<!--配置一个Servlet-->
<servlet>
    <servlet-name>config</servlet-name>
    <servlet-class>com.thr.ServletConfigDemo</servlet-class>
    <!-- 初始参数:这些参数会在加载web应用的时候,封装到ServletConfig对象中 -->
    <init-param>
        <param-name>name</param-name>
        <param-value>tanghaorong</param-value>
    </init-param>
    <init-param>
        <param-name>password</param-name>
        <param-value>123456</param-value>
    </init-param>
    <!-- 让servlet对象自动加载,注意:init-param要在自动加载之前 -->
    <load-on-startup>1</load-on-startup>
</servlet>
<!--配置Servlet的映射-->
<servlet-mapping>
    <servlet-name>config</servlet-name>
    <!-- Servlet的对外访问路径 -->
    <url-pattern>/config</url-pattern>
</servlet-mapping>

②、获取web.xml中标签初始化参数,代码如下:

package com.thr;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;

/**
 * @author tanghaorong
 * @date 2020-04-22
 * @desc 使用ServletConfig对象获取初始化参数
 */
public class ServletConfigDemo extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request,response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        //第一种方式——getInitParameter
        ServletConfig config = this.getServletConfig();
        String name = config.getInitParameter("name");
        String password = config.getInitParameter("password");
        System.out.println(name+":"+password);

        // 设置response的编码
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html");
        // 获取PrintWriter对象
        PrintWriter out = response.getWriter();
        // 输出信息
        out.println("<HEAD><TITLE>初始化信息</TITLE></HEAD>");
        out.println("姓名:" + name + "<br>");
        out.println("密码:" + password + "<br><hr>");


        //第二种方式——getInitParameterNames
        Enumeration<String> initParameterNames = this.getServletConfig().getInitParameterNames();
        while (initParameterNames.hasMoreElements()) {
            String names = initParameterNames.nextElement();
            String value = config.getInitParameter(names);
            System.out.println(value);
            response.setCharacterEncoding("UTF-8");
            response.setContentType("text/html");
            response.getWriter().print(names + "=" + value + "<br/>");
        }

        // 释放PrintWriter对象
        out.flush();
        out.close();
    }
}

③、启动 Tomcat 服务器,在浏览器的地址栏中输入地址: http://localhost:8080/config 访问Servlet,结果如图所示。

image

从上图中可以看出,web.xml 文件中配置的信息全部被读取了出来。

3.6 ServletContext对象

ServletContext对象表示的当前整个上下文(应用程序),也就是整个Web应用。这个对象在Tomcat启动的时候,会创建一个唯一的ServletContext对象代表当前的整个Web应用,该对象封装了当前Web应用的所有信息。我们一般用来配置或者获取整个应用的初始化配置信息、读取资源文件、多个Servlet之间的通信等。所以ServletContext是相对于整个的应用,而ServletConfig是单个的应用

image

下面对ServletContext对象获取不同的资源分别进行讲解。

3.6.1 获取Web应用程序的初始化参数

我们在web.xml文件中,不仅可以配置Servlet的映射信息和初始化信息,也可以配置整个Web应用的初始化信息。Web应用初始化参数的配置方式具体如下所示:

<!--配置整个Web应用的初始化信息格式-->
<context-param>
    <param-name>AAA</param-name>
    <param-value>BBB</param-value>
</context-param>
<context-param>
    <param-name>CCC</param-name>
    <param-value>DDD</param-value>
</context-param>

注意:在上面的配置文件中,<context-param> 元素位于根元素 <web-app>中,它的子元素 <param-name><param-value>分别用于指定参数的名字和参数值。要想获取这些参数名和参数值的信息,可以使用 ServletContext对象中定义的 getInitParameterNames()getInitParameter(String name)方法分别获取。

下面通过案例演示如何使用 ServletContext对象获取Web应用程序的初始化参数。

①、在项目的web.xml文件中配置初始化参数信息和Servlet信息,其代码如下所示:

<!--配置整个Web应用的初始化信息-->
<context-param>
    <param-name>name</param-name>
    <param-value>tanghaorong</param-value>
</context-param>
<context-param>
    <param-name>password</param-name>
    <param-value>123456</param-value>
</context-param>

<!--配置一个Servlet-->
<servlet>
    <servlet-name>context</servlet-name>
    <servlet-class>com.thr.ServletContextDemo</servlet-class>
    <!-- 让servlet对象自动加载,注意:init-param要在自动加载之前 -->
    <load-on-startup>1</load-on-startup>
</servlet>

<!--配置Servlet的映射-->
<servlet-mapping>
    <servlet-name>context</servlet-name>
    <!-- Servlet的对外访问路径 -->
    <url-pattern>/context</url-pattern>
</servlet-mapping>

②、使用ServletContext对象获取web.xml中配置的信息,代码如下所示。

package com.thr;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;

/**
 * @author tanghaorong
 * @date 2020-04-23
 * @desc 使用ServletContext对象获取整个Web应用的配置信息
 */
public class ServletContextDemo extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request,response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //获取ServletContext对象
        ServletContext context = this.getServletContext();
        Enumeration<String> names = context.getInitParameterNames();
        while (names.hasMoreElements()) {
            //获取配置信息
            String name = names.nextElement();
            String value = context.getInitParameter(name);

            //打印
            System.out.println(name+":"+value);
            response.setCharacterEncoding("UTF-8");
            response.setContentType("text/html");
            response.getWriter().println("<head><title>获取整个Web应用初始化信息</title></head>");
            response.getWriter().println(name + "=" + value + "<br/>");
            //释放流资源
            response.getWriter().close();
        }
    }
}

上述代码中,当通过 this.getServletContext() 方法获取到 ServletContext 对象后,首先调用getInitParameterNames()方法,获取到包含所有初始化参数名的 Enumeration 对象,然后遍历 Enumeration 对象,根据获取到的参数名,通过 getInitParamter(String name)方法得到对应的参数值。

③、启动 Tomcat 服务器,在浏览器的地址栏中输入地址 http://localhost:8080/context 访问,浏览器的显示结果如图所示。

image

从图中可以看出,web.xml 文件中配置的信息被读取了出来。

3.6.2 读取 Web 应用下的资源文件

我们在实际开发过程中,不仅需要从web.xml文件中配置信息,有时候也会会需要读取 Web 应用中的一些资源文件,如配置文件和日志文件等。为此,在 ServletContext 接口中定义了一些读取 Web 资源的方法,这些方法是依靠 Servlet 容器实现的。Servlet 容器根据资源文件相对于 Web 应用的路径,返回关联资源文件的 I/O 流或资源文件在系统的绝对路径等。

ServletContext对象中用于获取资源路径的相关方法。

  • Set getResourcePaths(String path):返回一个 Set 集合,集合中包含资源目录中子目录和文件的路径名 称。参数 path 必须以正斜线(/)开始,指定匹配资源的部分路径
  • String getRealPath(String path)返回资源文件在服务器文件系统上的真实路径(文件的绝对路径)。参数 path 代表资源文件的虚拟路径,它应该以正斜线(/)开始,/ 表示当前 Web 应用的根目录,如果 Servlet 容器不能将虚拟路径转换为文件系统的真实路径,则返回 null
  • URL getResource(String path):返回映射到某个资源文件的 URL 对象。参数 path 必须以正斜线(/)开始,/ 表示当前 Web 应用的根目录
  • InputStream getResourceAsStream(String path):返回映射到某个资源文件的 InputStream 输入流对象。参数 path 的传递规则和 getResource() 方法完全一致

关于getRealPath(),首先就要谈谈它到底是用来干什么的?那么我们就应该先了解一个网站关于虚拟路径的概念。

在一个网站中,假设我们访问http://localhost:8080/myWeb/test.jsp,那么从这个URL地址中我们看到整个的项目名叫myWeb,也知道他的虚拟路径在tomcat服务器的webapp根路径下。但是实际路径到底是不是在tomcat的webapp路径下呢?

答案:可以是也可以不是。

我们默认可以把项目加载到tomcat下的webapp下,但是也可以通过tomcat的server.xml去将一个实际的物理路径映射到tomcat的webapp下。在server.xml中配置如下就可以了:<Context path="/myWeb" docBase="E:/aaa" debug="0" reloadable="true"/>

那么谈了这么多getRealPath()是干什么的呢?

答案显而易见了,他就是用来获取网站的实际物理路径的。

通常我们可以通过request.getSession().getServletContext().getRealPath()来获取网站的物理路径。例如myWeb项目的物理路径被配置在E:/aaa下,那么我们使用getRealPath()得到的就是“E:/aaa”。

getRealPath("/upload")也可以有参数,作用就是获取在项目根路径下的子文件夹的物理路径。即E:/aaa/upload。

详细可见: request.getSession().getServletContext().getRealPath("/");_路在何方い的博客-CSDN博客_request.getsession.getservlet

熟悉了下面的方法后,在通过使用 ServletContext 对象读取资源文件举例:

在项目的src目录下创建一个名称为userInfo.properties的文件,文件中的配置信息如下:

name=tanghaorong
password=654321

②、使用ServletContext对象获取userInfo.properties中的资源文件配置信息,代码如下所示:

package com.thr;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
 * @author tanghaorong
 * @date 2020-04-23
 * @desc 使用ServletContext对象获取整个Web应用的配置信息
 */
public class ServletContextDemo1 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request,response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {


        //第一种方式:使用ServletContext对象读取资源文件
        ServletContext context = this.getServletContext();
        //获取userInfo.properties文件
        InputStream is = context.getResourceAsStream("/WEB-INF/classes/userInfo.properties");
        //创建Properties并载入数据
        Properties prop = new Properties();
        prop.load(is);

        //打印在网页上
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html");
        response.getWriter().println("<head><title>使用ServletContext对象读取资源文件</title></head>");
        response.getWriter().println("姓名"+":"+prop.getProperty("name")+ "<br/>");
        response.getWriter().println("密码"+":"+prop.getProperty("password")+ "<br/>");



        //第二种方式:使用类装载器读取资源文件
        ClassLoader loader = ServletContextDemo1.class.getClassLoader();
        InputStream resourceAsStream = loader.getResourceAsStream("userInfo.properties");
        Properties properties = new Properties();
        properties.load(resourceAsStream);
        String name = properties.getProperty("name");
        String password = properties.getProperty("password");
        //在控制台打印
        System.out.println("姓名:"+name+",密码:"+password);
    }
}

③、启动 Tomcat 服务器,在浏览器的地址栏中输入地址 http://localhost:8080/context1 访问,浏览器的显示结果如图所示。

image

从图中可以看出,userInfo.properties 资源文件中的内容已经被读取了出来。

3.6.3 多个Servlet之间的通信

ServletContext代表了整个Web应用,并且数据是共享的,而一个Web应用可以有多个Servlet实例,也就意味着多个Servlet是可以实现通信的。下面使用ServletContext实现多个Servlet之间的通信,相关的方法如下。

  • Object getAttribute(String var1):获取域对象中共享的数据
  • void setAttribute(String var1, Object var2):向域对象中共享数据
  • void removeAttribute(String var1):删除域对象中共享的数据

我们创建两个类ServletContextDemo2和ServletContextDemo3通过ServletContext对象实现通信。

ServletContextDemo2代码如下:

package com.thr;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
 * @author tanghaorong
 * @date 2020-04-23
 * @desc 使用ServletContext对象实现多个Servlet通信,放入
 */
public class ServletContextDemo2 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request,response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        String name="tanghaorong";
        String password="123456";
        ServletContext context = this.getServletContext();
        //向域对象中共享数据
        context.setAttribute("name",name);
        context.setAttribute("password",password);
    }
}

ServletContextDemo3代码如下:

package com.thr;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
 * @author tanghaorong
 * @date 2020-04-23
 * @desc 使用ServletContext对象实现多个Servlet通信,获取
 */
public class ServletContextDemo3 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request,response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        ServletContext context = this.getServletContext();
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html");
        //获取域对象中共享的数据
        response.getWriter().println("姓名"+":"+context.getAttribute("name")+ "<br/>");
        response.getWriter().println("密码"+":"+context.getAttribute("password")+ "<br/>");


    }
}

先运行ServletContextDemo2,并且访问该Servlet,将数据name和password数据存储到ServletContext对象中。然后运行访问ServletContextDemo3,就可以从ServletContext对象中取出数据了,这样就实现了多个Servlet的通信,运行结果如下图所示:

image

3.7 Servlet,GenericServlet和HTTPServlet

Servlet,GenericServlet和HTTPServlet三者之间的关系如下图。

image

①、Servlet接口是Servlet程序的根接口,里面定义了5个方法。

public interface Servlet {
    //初始化
    void init(ServletConfig var1) throws ServletException;
    //获取ServletConfig对象
    ServletConfig getServletConfig();
    //用于处理请求和响应
    void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
    //获取Servlet的相关信息
    String getServletInfo();
    //销毁
    void destroy();
}

②、GenericServlet实现了Servlet接口和ServletConfig接口,它是一个抽象类,将Servlet接口中的init()、destroy()、getServletConfig()、getServletInfo()进行了重写,并且将service()设置为抽象方法,因此若创建的Servlet继承了GenericServlet,则只需要重写service()即可,但是在实际中一般不会使用它,因为它有一个子类HttpServlet,功能更加强大。

GenericServlet抽象类相比于直接实现Servlet接口,有以下几个好处:

  • 为Servlet接口中的所有方法提供了默认的实现,则程序员需要什么就直接改什么,不再需要把所有的方法都自己实现了。
  • 提供了一系列的方法,包括ServletConfig对象中的方法。
  • 将init( )方法中的ServletConfig参数赋给了一个内部的ServletConfig引用从而来保存ServletConfig对象,不需要程序员自己去维护ServletConfig了。
package javax.servlet;

import java.io.IOException;
import java.io.Serializable;
import java.util.Enumeration;
import java.util.ResourceBundle;

public abstract class GenericServlet implements Servlet, ServletConfig, Serializable {
    private static final String LSTRING_FILE = "javax.servlet.LocalStrings";
    private static ResourceBundle lStrings = ResourceBundle.getBundle("javax.servlet.LocalStrings");
    private transient ServletConfig config;

    public GenericServlet() {
    }

    public void destroy() {
    }

    public String getInitParameter(String name) {
        ServletConfig sc = this.getServletConfig();
        if (sc == null) {
            throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
        } else {
            return sc.getInitParameter(name);
        }
    }

    public Enumeration<String> getInitParameterNames() {
        ServletConfig sc = this.getServletConfig();
        if (sc == null) {
            throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
        } else {
            return sc.getInitParameterNames();
        }
    }

    public ServletConfig getServletConfig() {
        return this.config;
    }

    public ServletContext getServletContext() {
        ServletConfig sc = this.getServletConfig();
        if (sc == null) {
            throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
        } else {
            return sc.getServletContext();
        }
    }

    public String getServletInfo() {
        return "";
    }

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

    public void init() throws ServletException {
    }

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

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

    public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

    public String getServletName() {
        ServletConfig sc = this.getServletConfig();
        if (sc == null) {
            throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
        } else {
            return sc.getServletName();
        }
    }
}

③、HttpServlet继承了GenericServlet,还记得在GenericServlet中的service()方法,将其定义为了一个抽象的方法,所以在HttpServlet中肯定会进行重写,然后来具体的看一看HttpServlet抽象类是如何实现自己的service方法吧。

首先来看GenericServlet抽象类中是如何定义service方法的:

public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

HttpServlet又是怎么重写这个service方法的:

public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
    if (req instanceof HttpServletRequest && res instanceof HttpServletResponse) {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        this.service(request, response);
    } else {
        throw new ServletException("non-HTTP request or response");
    }
}

可以发现 HttpServlet在重写GenericServlet中的service()时,将ServletRequest强制转成了HttpServletRequest,ServletResponse强制转成了HttpServletResponse,之所以将它两转为Http类型的因为它们的功能更加强大。然后 又调用了一个拥有HttpServletRequest和HttpServletResponse为参数的service(),再来看看这个方法是如何实现的:

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    //获取请求的类型;GET|POST|PUT|DELETE...
    String method = req.getMethod();
    long lastModified;
    //是否为GET请求
    if (method.equals("GET")) {
        lastModified = this.getLastModified(req);
        if (lastModified == -1L) {
            this.doGet(req, resp);
        } else {
            long ifModifiedSince = req.getDateHeader("If-Modified-Since");
            if (ifModifiedSince < lastModified) {
                this.maybeSetLastModified(resp, lastModified);
                this.doGet(req, resp);
            } else {
                resp.setStatus(304);
            }
        }
        //是否为HEAD请求
    } else if (method.equals("HEAD")) {
        lastModified = this.getLastModified(req);
        this.maybeSetLastModified(resp, lastModified);
        this.doHead(req, resp);
        //是否为POST请求
    } else if (method.equals("POST")) {
        this.doPost(req, resp);
    } else if (method.equals("PUT")) {
        this.doPut(req, resp);
    } else if (method.equals("DELETE")) {
        this.doDelete(req, resp);
    } else if (method.equals("OPTIONS")) {
        this.doOptions(req, resp);
    } else if (method.equals("TRACE")) {
        this.doTrace(req, resp);
    } else {
        String errMsg = lStrings.getString("http.method_not_implemented");
        Object[] errArgs = new Object[]{method};
        errMsg = MessageFormat.format(errMsg, errArgs);
        resp.sendError(501, errMsg);
    }

}

在这个方法中,先获取了当前请求的请求方式,通过判断请求的方式调用不同的方法,分别调用了doXXX()方法,例如:GET请求调用了doGet(),POST请求调用了doPost(),由于浏览器只能发送GET和POST请求,因此若Servlet继承了HttpServlet,只需要重写其中的doGet()和doPost()即可。

3.8 HttpServletRequest接口

HttpServletRequest表示Http环境中的Servlet请求。它是一个接口,继承自javax.servlet.ServletRequest接口,它封装了请求报文,因此可以通过此对象获取请求报文中的数据以及请求转发。HttpServletRequest在ServletRequest接口的基础上添加下面这几个方法:

  • String getContextPath()返回请求上下文的请求URI部分
  • Cookie[] getCookies()返回一个cookie对象数组
  • String getHeader(String var1):返回指定HTTP标题的值
  • String getMethod():返回生成这个请求HTTP的方法名称
  • String getQueryString():返回请求URL中的查询字符串
  • HttpSession getSession()返回与这个请求相关的会话对象

3.8.1 HttpServletRequest内封装的请求

image

3.8.2 通过request获得请求行

获得请求行的相关方法如下:

  • String getMethod():获取请求的方式(get/post)
  • String getRequestURI()获取请求的URI地址
  • StringBuffer getRequestURL()获取请求的URL地址
  • String getContextPath()获取web应用的名称
  • String getQueryString():获取get提交url地址后的参数字符串
  • URI,统一资源标志符(Uniform Resource Identifier, URI),表示的是web上每一种可用的资源,如 HTML文档、图像、视频片段、程序等都由一个URI进行标识的。
  • URL,统一资源定位符(Uniform Resource Locator,URL),是URI的一个子集。
  • 只要能唯一标识资源的就是URI,在URI的基础上给出其资源的访问方式的就是URL

request获得请求行示例代码:

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println(req.getMethod());
        System.out.println(req.getRequestURI());
        System.out.println(req.getRequestURL());
        System.out.println(req.getContextPath());
        System.out.println(req.getQueryString());
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }

获取结果:

image

3.8.3 通过request获得请求头

获得请求头的相关方法如下:

  • String getHeader(String name):根据请求头的key获取对应的value
  • Enumeration getHeaderNames():获取请求头中所有的key
  • Enumeration getHeaders(String name):根据请求头的key获取对应批量的value
  • int getIntHeader(String name):根据请求头的key获取对应的value,它返回的是Int类型的值
   @Override
   protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
       //根据请求头的key获取对应的value
       System.out.println(req.getHeader("Host"));
       //获取请求头中所有的key
       Enumeration<String> names = req.getHeaderNames();
       while (names.hasMoreElements()){
           System.out.println(names.nextElement());
       }
       //根据请求头的key获取对应批量的value
       Enumeration<String> accept = req.getHeaders("Accept");
       while (accept.hasMoreElements()){
           System.out.println(accept.nextElement());
       }
       //根据请求头的key获取对应的value,它返回的是Int类型的值
       System.out.println(req.getIntHeader("Content-length"));
   }
   @Override
   protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
       doGet(req, resp);
   }

image

补充:HTTP请求中的常用消息头

  • accept:浏览器通过这个头告诉服务器,它所支持的数据类型
  • Accept-Charset: 浏览器通过这个头告诉服务器,它支持哪种字符集
  • Accept-Encoding:浏览器通过这个头告诉服务器,支持的压缩格式
  • Accept-Language:浏览器通过这个头告诉服务器,它的语言环境
  • Host:浏览器通过这个头告诉服务器,想访问哪台主机
  • If-Modified-Since: 浏览器通过这个头告诉服务器,缓存数据的时间
  • Referer:浏览器通过这个头告诉服务器,客户机是哪个页面来的 防盗链
  • Connection:浏览器通过这个头告诉服务器,请求完后是断开链接还是何持链接

3.8.4 通过request获得请求体

上面请求体中的内容是通过post提交的请求参数,格式是:

username=tanghaorong&password=123456
#对应的key-value格式为
username:tanghaorong
password:123456

以上面参数为例,通过一下方法获得请求参数:

  • String getParameter(String name):根据请求体中的key获取value
  • String[] getParameterValues(String name):根据请求体中的key获取批量value
  • Enumeration getParameterNames():获取所有请求体中的key
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    //根据请求体中的key获取value
    System.out.println(req.getParameter("username"));
    System.out.println(req.getParameter("password"));
    //根据请求体中的key获取批量value
    String[] usernames = req.getParameterValues("username");
    for (String username : usernames) {
        System.out.println(username);
    }
    //获取所有请求体中的key
    Enumeration<String> names = req.getParameterNames();
    while (names.hasMoreElements()){
        System.out.println(names.nextElement());
    }
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    doGet(req, resp);
}

3.8.5 Request乱码问题的解决方法

在service中使用的编码解码方式默认为:ISO-8859-1编码,但此编码并不支持中文,因此会出现乱码问题,所以我们需要手动修改编码方式为UTF-8编码,才能解决中文乱码问题,下面是发生乱码的具体细节:

乱码问题解决:

  • 解决get提交的方式的乱码:
    • 在tomcat的配置文件server.xml,在71行左右,改端口号的标签中,加入属性URIEncoding="UTF-8"
    • 或者使用String parameter = new String(parameter.getbytes("iso8859-1"),"utf-8");
  • 解决post提交方式的乱码:
    • 在获取请求参数之前设置request.setCharacterEncoding("UTF-8");

3.9 HttpServletResponse

HttpServletResponse也是一个接口,它继承自ServletResponse接口,专门用来封装HTTP响应报文,由于HTTP请求消息分为状态行,响应消息头,响应消息体三部分,因此,在HttpServletResponse接口中定义了向客户端发送响应状态码,响应消息头,响应消息体的方法。

3.9.1 HttpServletResponse内封装的响应

image

image

3.9.2 Response的乱码问题解决

//设置响应报文的编码格式
response.setCharacterEncoding("UTF-8");
//设置响应报文中响应体的内容格式以及浏览器的解码方式
response.setContentType("text/html;charset=UTF-8");
//向浏览器响应数据,就是将数据以响应体的方式响应到浏览器
PrintWriter writer = response.getWriter();
writer.print("helloworld<br>");
writer.write("你好");

3.10 Servlet的转发和重定向

转发:客户端向服务器端发送请求,服务器将请求转发到服务器内部,再响应给客户端。

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {
    //转发
    request.getRequestDispatcher("success.jsp").forward(request,response);
}

访问后的地址:

image

重定向:客户端向服务器端发送请求,服务器告诉客户端你去重定向(状态码302响应头location=客户端绝对路径),客户端继续向服务器发送请求(请求地址已经成重定向的地址),服务器端给客户端响应。

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {

    //重定向的两种方式
    //方式一
    //getContextPath():获取web应用的名称 如:/servlet01
    //resp.sendRedirect(req.getContextPath()+"/success.html");
    response.sendRedirect("https://www.baidu.com/");

    //方式二
    //response.setStatus(302);
    //response.setHeader("location","https://www.baidu.com/");
}

访问后的地址:

image

转发和重定向的区别:

  • 请求次数:转发只发出了一次请求,而重定向发出了两次请求。
  • 地址栏变化:转发不会改变地址栏中的URL,而重定向则会改变URL。
  • 项目名称:转发不用写项目名称(默认:http://localhost:8080/项目名称/),重定向需要编写项目名称(默认:http://localhost:8080/)。
  • 跳转范围:转发只能访问到当前web应用中的内容,而重定向则可以访问到任意web应用中的内容 。
  • request对象作用范围:转发后,在转发后的页面中仍然可以使用原来的request对象,而重定向,原来的request对象则失去作用。

3.11 补充:web应用中的路径问题(重要)

路径分为相对路径和绝对路径:

  • 相对路径:目标资源相当于当前位置的路径
  • 绝对路径
    • Static web:绝对路径指资源在磁盘上的完整路径
    • Web Application:绝对路径指资源在服务器中的路径,以/开头的路径都是绝对路径

3.11.1 相对路径的缺点

1、对于相同的资源,在不同的页面中访问路径不同,即对于同一个资源没有统一的访问路径

2、若当前位置或目标资源的位置发生了变化,则相对路径有可能失效

3、由于转发浏览器发送一次请求,且在服务器的内部跳转到转发的地址,因此地址栏不变,即访问servlet的地址,因此造成了地址栏中的地址和页面中显示的内容不匹配,即当前位置发生了变化,就影响了页面中所有的相对路径

总结:相对路径不靠谱,推荐使用绝对路径

3.11.2 绝对路径

在web应用中,以 / 开头的路径都是绝对路径。绝对路径又分为由浏览器解析的绝对路径由服务器解析的绝对路径

  • 由浏览器解析的绝对路径,/ 表示localhost:8080下访问

    • 由浏览器解析的绝对路径的情况有:html标签中所设置的绝对路径(超链接、form标签中的action、img、link、script)、JavaScript中的location对象所设置的绝对路径、重定向中设置的绝对路径(可以理解为静态的)
    <a href="/HelloServlet">HelloServlet</a>
    <--浏览器会将其解析的地址为:localhost:8080/HelloServlet,如果你有上下文路径的话则会报404错误!(上下文路径就是你的项目名称)-->
    
  • 由服务器解析的绝对路径,/ 表示localhost:8080/上下文路径 下访问(上下文路径就是你的项目名称

    • 由服务器解析的绝对路径的情况有:web.xml中url-pattern设置的绝对路径、转发中设置的绝对路径、jsp中jsp指令和jsp动作标签所设置的绝对路径(可以理解为动态的)

3.11.3 浏览器解析的绝对路径的解决方案

页面中所设置的绝对路径:手动添加上下文路径,例如:

<a href="${pageContext.request.contextPath}/success.jsp">success.jsp</a>

重定向中所设置的绝对路径:在绝对路径前通过request.getContextPath()拼接上下文路径,例如:

resp.sendRedirect(req.getContextPath() + "/success.jsp");

参考资料:

4、JSP

4.1 前言

大家应该都很清楚,JSP也是一种古老的技术了,在实际的项目中现在基本不用了,或许在很老的项目中可以看到。而现在在前后端分离的时代,后端只需要返回JSON给前端就可以了,页面完全不需要后端管。那我们还需要学习JSP吗?要呀!尤其对于新手来说,JSP肯定要学习的,但是可以不用深入的学习JSP的各种内容,但至少再看到JSP的时候,你能知道什么是JSP,能看懂JSP的代码。而且对于新手来说,刚刚开始学习JavaWeb的时候,前端页面更多使用的是JSP。所以不要求JSP很精通,但起码看到要会使用吧。虽然现在市面上有许多代替JSP的产品了,如常见的模板引擎如:Freemarker、Thymeleaf、Velocity等。它的用法跟JSP差不太多,但JSP出现的最早,它是大哥,所以我们还是跟着大哥一步一步的走吧。

4.2 JSP技术的简介

JSP是Java Server Pages的简称,即Java服务器页面。它是由Sun公司倡导的一种用于开发动态网页的技术标准。其特点就是在传统的网页HTML文件(.htm、.html)中插入Java程序段(Scriptlet)和JSP标记(Tag)。从而形成JSP文件(*.jsp)。其中HTML提供静态的数据,而JSP技术允许在页面中嵌套Java代码,所以它提供动态数据,不过一般不会在JSP页面中编写Java代码,只是在JSP页面中动态获取数据。

其实JSP本身就是一种Servlet。因为JSP在被访问后会被编译为一个Java类,该类继承了HttpJspBase,而HttpJspBase类又继承了HttpServlet。所以我们访问JSP时,其实不是在访问JSP,而是在访问JSP编译过后的那个Servlet。因为Servlet输出HTML非常困难,所以JSP就是替代Servlet输出HTML的。

JSP技术的特点:JSP技术所开发的web应用程序是基于Java的,它拥有Java跨平台的特性,以及业务代码分离,组建重用,基础Java Servlet功能和预编译功能。

  1. 跨平台:由于JSP是基于Java语言的,因而它可以使用Java的API,所以也是跨平台的,可以应用在Windows、Linux、Mac和Solaris。
  2. 业务代码分离:采用JSP开发的项目,通常使用HTML语言来设计和格式化静态页面内容,而使用JSP标签来实现动态部分,业务代码通常使用Servlet、Struts、Springmvc等业务控制层来处理,从而实现业务层和视图层分离,这样,JSP只负责显示数据即可,这样,修改业务代码不会影响JSP页面代码。
  3. 组件重用:JSP中,可以使用JavaBean编写业务组件,也就是使用一个JavaBean封装业务处理代码或者作为一个数据处理模型,这个JavaBean可以重复使用,也可以应用到其他应用程序中。
  4. 继承Java Servlet功能:JSP的本质是Servlet,因此说JSP拥有Servlet的所有功能。
  5. 预编译:用户首次通过浏览器访问JSP页面时,服务器对JSP页面代码进行编译,并且仅执行一次编译,编译后被保存,下次访问时直接执行编译过的代码,节约了服务器资源,提升了客户端访问速度。

4.3 JSP的基础语法

4.3.1 JSP的注释

在jsp中常用的注释有两种:显示注释和隐式注释。

注释类型 描述
显示注释 HTML注释:<!- - 注释内容- ->
隐式注释 Java注释://单行注释 、/…多行注释…/
隐式注释 JSP专有注释:<%- - 注释内容- -%>

其中HTML注释在浏览器中查看源文件的时候是可以看得到的。而 Java 注释和 jsp 注释在浏览器中查看源文件时是看不到注释的内容的。

4.3.2 JSP的指令元素

JSP指令(Directives)主要用来提供整个JSP网页的信息,并用来设定JSP网页的相关属性,例如:设置网页的编码格式,语法,信息等。JSP指令的基本语法格式为:<%@ 指令 属性名="值" %> 。如果一个指令有多个属性,这多个属性可以写在一个指令中,也可以分开写,如下所示:

<%@ page contentType="text/html;charset=gb2312"%>
<%@ page import="java.util.Date"%>
<!-- 也可以合并来设置,如下所示:-->
<%@ page contentType="text/html;charset=gb2312" import="java.util.Date"%>

在 JSP 2.0规范中共定义了三个指令:

  • page指令
  • include指令
  • taglib指令

每个指令都有自己的属性,下面来介绍这三种指令。

①、page指令

我们通过page指令来设置JSP页面的属性,它作用整个JSP页面,所以为了保持程序的可读性和遵循良好的编程习惯,page指令最好是放在整个JSP页面的起始位置。page指令一共有13个属性,下面介绍一下:

属性 参数 描述
language java 主要用来指定用何种语言来解释JSP网页,目前只能有Java来解释
import {package.class | package.*}, ... 指定此JSP网页引入哪些Java API
extends package.class 指定此JSP网页产生的Servlet所继承的类
pageEncoding characterSet | ISO-8859-1 指定此JSP网页的编码属性
session true | false 指定此JSP网页是否使用session(默认true)
buffer none | 8kb | sizekb 指定此JSP输出流是否有缓冲区(默认有,大小8KB的缓冲区)
autoFlush true | false 指定此JSP输出流的缓冲区是否自动清除(默认true)
isThreadSafe true | false 已经不使用了(默认true)
info text 表示此JSP的网页信息
erroPage relative_url 如果网页发生异常错误,则会重定向到指定的URL地址,前提是isErrorPage为true
isErrorPage true | false 指定此JSP能否在发生错误是转向另一个URL(默认true)
contentType mimeType;charset=characterSet | text/html;charset=ISO-8859-1 指定此JSP网页的编码方式
isElIgnored true | false 表示此JSP在执行时是否忽略EL表示(默认true)

简单举例:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.util.*" pageEncoding="utf-8" %>

注意:这些指令必须包含在<%@page key=value …… %>中,而且只有import实现可以重复设置,其余属性只能设置一次或者不设置。

👏还有一点就是page指令中的errorPage属性。我们是可以在web.xml文件中使用<error-page>元素为整个Web应用程序设置错误处理页面。 <error-page>元素有3个子元素,<error-code><exception-type><location>

  • <error-code>子元素指定错误的状态码,例如:<error-code>404</error-code>
  • <exception-type>子元素指定异常类的完全限定名,例如:<exception-type>java.lang.ArithmeticException</exception-type>
  • <location>子元素指定以“/”开头的错误处理页面的路径,例如:<location>/404Error.jsp</location>
<!--处理404错误-->
<error-page>
    <error-code>404</error-code>
    <location>/404Error.jsp</location>
</error-page>
<!--处理500错误-->
<error-page>
    <error-code>500</error-code>
    <location>/500Error.jsp</location>
</error-page>

而如果你在某个JSP的页面设置了errorPage属性如:<%@ page errorPage="/error.jsp" %>,那么在web.xml文件中设置的错误处理将不对该页面起作用。

②、include指令

这个指令听名字是包含的意思。没错,include指令用于引入其它JSP页面,如果使用include指令引入了其它JSP页面,那么JSP引擎将把这两个JSP翻译成一个servlet。所以include指令引入通常也称之为静态引入。

语法:<%@ include file="relativeURL"%>,其中的file属性用于指定被引入文件的路径。路径以“/”开头,表示代表当前web应用。被引入的文件可以使用任意的扩展名,即使其扩展名是html。

include指令使用举例:创建main.jsp、head.jsp和foot.jsp页面,分别作为jsp页面主体、头部和尾部,存放于Web根目录下,代码如下:

<%-- main.jsp:--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head>
        <title>主网页</title>
    </head>
    <body>
        <%@ include file="head.jsp"%>
        <hr>
        <%@ include file="foot.jsp"%>
    </body>
</html>



<%-- head.jsp: --%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head>
        <title>这是网页head</title>
    </head>
    <body>
        这是网页head
    </body>
</html>



<%-- foot.jsp: --%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head>
        <title>网页foot</title>
    </head>
    <body>
        这是网页foot
    </body>
</html>

运行结果如下:

image

补充:还有一种方式引入页面:jsp:include指令,功能与@include类似,后面会介绍。@include指令和jsp:include指令的在于:

  • @include只是把别的页面内容包含进来,属于静态包含。
  • jsp:include为动态包含,如果被包含的页面是JSP,则先处理之后再将结果包含,而如果包含的是非*.jsp文件,则只是把文件内容静态包含进来。

③、taglib指令

在JSP中,可以直接使用JSP提供的元素来完成特定的功能,而通过使用taglib指令,我们就可以在页面中使用自定义的标签,将标签库描述符文件导入到JSP页面即可。taglib指令的使用格式如下:

<%@ taglib uri="tigLibURL" 或 tagDir="tagDir" prefix="tagPrefix" %>

参数说明:

  • uri属性:定位标签库描述符的位置。唯一标识和前缀相关的标签库描述符,可以使用绝对或相对URL。
  • tagDir属性:指示前缀将被用于标识在WEV-INF/tags目录下的标签文件。
  • prefix属性:标签的前缀,区分多个自定义标签。不可以使用保留前缀和空前缀,遵循XML命名空间的命名约定。

例如后面需要使用JSTL的代码:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:set var="name" value="hellojsp" />

上面就是通过<c:set />标签将"hellojsp”值赋给变量name。

4.3.3 JSP的表达式

JSP表达式、小脚本、声明这三者统称为JSP脚本表达式(expression),它用于将程序数据输出到客户端。下面对JSP表达式先进行讲解。

JSP表达式的语法:<%= Java表达式 %>。例如输出当前系统时间:

<%= new java.util.Date() %>

JSP引擎在翻译脚本表达式时,会将程序数据转成字符串,然后在相应位置用out.print(…) 将数据输给客户端。JSP脚本表达式中的变量或表达式后面不能有分号(😉。

4.3.4 JSP的小脚本

JSP脚本元素(基本不用)用来插入Java代码,这些Java代码将会出现在由当前JSP页面生成的Servlet中。

JSP小脚本的语法:<% 多行Java代码 %>。我们可以在里面定义变量、编写语句,调用方法,但是不能定义方法

<%
//声明变量
int sum=0;

/*编写语句*/
for (int i=1;i<=100;i++){
    sum+=i;
}
out.println("<h1>sum="+sum+"</h1>");
%>

多个脚本片断中的代码可以相互访问。单个脚本片断中的Java语句可以是不完整的,但是,多个脚本片断组合后的结果必须是完整的Java语句,例如:

<%
//声明变量
int sum=0;
/*编写语句*/
for (int i=1;i<=100;i++){
    %>
<%
sum+=i;
}
out.println("<h1>sum="+sum+"</h1>");
%>

4.3.5 JSP的声明

JSP的声明用来在JSP页面中声明变量和定义方法。JSP的声明语法:<%! Java代码 %>,简单举例:

<%! String color[]={"red","yellow","blue"};
String getColor(int i){
    return color[i];
}
%>

4.3.6 JSP的动作标签

JSP动作标签的格式为:

<jsp:标签名 属性名="属性值" 属性名="属性值"></jsp:标签名>

在JSP中的动作标签包括有这几个:<jsp:param>、<jsp:include>、<jsp:forward>、<jsp:UseBean>、<jsp:getProperty>、<jsp:setProperty>、<jsp:plugin>。下面只介绍<jsp:include><jsp:forward>这两个,因为其它的几乎不会用到。

  • <jsp:include>:在前面提到过,它可以包含一个静态或动态的文件。格式:<jsp:include page=”URL”>
<jsp:include page="/include.jsp"></jsp:include>
  • <jsp:forward>:表示重定向到一个静态的HTML/JSP文件。格式:<jsp:forward page=”URL”>
<jsp:forward page="/index.jsp"></jsp:forward>

4.4 JSP的9大内置对象

JSP页面中一共内置了9个对象,分别为:out、session、response、request、config、page、application、pageContext、exception。在所有的JSP页面均可使用

但是必须在脚本元素的表达式或代码段中才可使用(<%=使用内置对象%>或<%使用内置对象%>内使用)。

而这9大内置对象又分为四类类型:

  • 输入输出对象:out、response、request。
  • 通信控制对象:pageContext、session、application。
  • Servlet对象:page、config。
  • 错误处理对象:exception。

下面依次介绍这9个内置对象的使用:

对象名 类型 描述
out javax.servlet.jsp.JspWriter 向客客户端、浏览器输出数据。
request javax.servlet.ServletRequest 封装了来自客户端、浏览器的各种信息。
response javax.servlet.SrvletResponse 封装了服务器的响应信息。
session javax.servlet.http.HttpSession 用来保存每个用户的信息,以便跟踪每个用户的操作状态。
application javax.servlet.ServletContext 代表了当前应用程序的上下文。可以在不同的用户之间共享信息。
exception javax.lang.Throwable 封装了JSP程序执行过程中发生的异常和错误信息。
config javax.servlet.ServletConfig 封装了应用程序的配置信息。
page javax.lang.Object JSP实现类的实例,它是当前JSP程序本身,通过这个可以对它进行访问。
pageContext javax.servlet.jsp.PageContext 为JSP页面包装页面的上下文。管理对属于JSP中特殊可见部分中己经命名对象的该问。

①、out对象

out对象是输出流,能把结果输出到网页上。获取方法: PrintWriter out = response.getWriter(); 常用的方法有两个:print(Object obj)println(Object obj)。两者的区别就是out.println(Object obj)会在输出后的末尾加上换行符,而print则不会。

out对象常用的方法如下:

  1. void clear():清除缓冲区的内容
  2. void clearBuffer():清除缓冲区的当前内容
  3. void flush()将缓冲内容flush到客户端浏览器
  4. int getBufferSize():返回缓冲大小,单位KB
  5. int getRemaining():返回缓冲剩余大小,单位KB
  6. isAutoFlush():返回缓冲区满时,是自动清空还是抛出异常
  7. void close()关闭输出流

②、request对象

request表示客户端的请求。它包含了客户端的信息以及请求的信息,如请求那个文件,附带的地址参数等。每次客户端的请求都会产生一个request实例。request对象的常用方法如下:

  1. object getAttribute(String name)返回指定属性的属性值
  2. Enumeration getAttributeNames():返回所有可用属性名的枚举
  3. String getCharacterEncoding():返回字符编码方式
  4. int getContentLength():返回请求体的长度(以字节数)
  5. String getContentType():得到请求体的MIME类型
  6. ServletInputStream getInputStream():得到请求体中一行的二进制流
  7. String getParameter(String name):返回name指定参数的参数值
  8. Enumeration getParameterNames():返回可用参数名的枚举
  9. String[] getparameterValues(String name):返回包含参数name的所有值的数组
  10. String getProtocol():返回请求用的协议类型及版本号
  11. String getScheme():返回请求用的计划名,如:http https及ftp等
  12. int getServerPort():返回服务器接受此请求所用的端口号
  13. String getServerName():返回接受请求的服务器主机名
  14. BufferedReader getReader():返回解码过了的请求体
  15. String getRemoteAddr()返回发送此请求的客户端IP地址
  16. String getRemoteHost()返回发送此请求的客户端主机名
  17. void setAttribute(String key Object obj)设置属性的属性值
  18. String getRealPath(String path)返回一虚拟路径的真实路径
  19. void setCharacterEncoding(“gb2312”)设置接受参数的字符集

③、response对象

response对象代表客户端的响应。服务器端的任何输出都通过response对象发送到客户端浏览器。每次服务器端都会响应一个response实例。response对象的常用方法如下:

  1. String getCharacterEncoding():返回响应用的是何种字符编码
  2. ServletOutputStream getOutputStream():返回响应的一个二进制输出流
  3. PrintWriter getWriter()返回可以向客户端输出字符的一个对象
  4. void setContentLength(int len):设置响应头长度
  5. void setContentType(String type):设置响应的MIME类型
  6. sendRedirect(java.lang.String location)重新定向客户端的请求
  7. void setCharacterEncoding(“gb2312”)设置响应头的字符集

④、session对象

session与cookie是记录客户访问信息的两种机制session是用于服务器端保存用户信息cookie用于在客户端保存用户信息。Servlet中通过request.getSession()来获取session对象,而JSP中可以直接使用。

如果JSP中配置了<%@page session=”false”%>,则隐藏对象session不可用。每个用户对应一个session对象。session对象的常用方法如下:

  1. long getCreationTime():返回Session创建时间
  2. public String getId():返回Session创建时JSP引擎为它设的唯一ID号
  3. long getLastAccessedTime():返回此Session里客户端最近一次请求时间
  4. int getMaxInactiveInterval():返回两次请求间隔多长时间此Session被取消(ms)
  5. String[] getValueNames():返回一个包含此Session中所有可用属性的数组
  6. void invalidate()取消Session,使Session不可用
  7. boolean isNew():返回服务器创建的一个Session,客户端是否已经加入
  8. void removeValue(String name)删除Session中指定的属性
  9. void setAttribute(String key,Object obj)设置Session的属性
  10. Object getAttribute(String name)返回session中属性名为name的对象

⑤、application对象

application封装JSP所在Web应用程序的信息,例如web.xml中配置的全局的初始化信息。Servlet中application对象需要通过ServletConfig.getServletContext()来获取。整个Web应用程序对应一个application对象。application对象常用的方法如下:

  1. Object getAttribute(String name):返回application中属性为name的对象
  2. Enumeration getAttributeNames():返回application中的所有属性名
  3. void setAttribute(String name,Object value):设置application属性
  4. void removeAttribute(String name):移除application属性
  5. String getInitParameter(String name):返回全局初始话函数
  6. Enumeration getInitParameterNames():返回所有的全局初始话参数
  7. String getMimeType(String filename):返回文件的文档类型,例如getMimeType(“abc.html”)将返回“text.html”
  8. String getRealPath(String relativePath):返回Web应用程序内相对网址对应的绝对路径

⑥、exception对象

exception封装了JSP中抛出的异常信息。要使用exception隐藏对象,需要设置<%@page isErrorPage”true”%>。隐藏对象exception通常被用来处理错误页面。

⑦、config对象

隐藏对象config是javax.servlet.ServletConfig类的实例,ServletConfig封装了配置在web.xml中初始化JSP的参数。JSP中通过config获取这些参数。每个JSP文件中共有一个config对象。config对象的常用方法如表:

  1. String getInitParameter(String name):返回配置在web.xml中初始化参数
  2. Enumeration getInitParameterNames():返回所有的初始化参数名称
  3. ServletContext getServletContext():返回ServletContext对象
  4. String getServletName:返回Servlet对象

⑧、page对象

page对象表示当前JSP页面,是当前JSP编译后的Servlet类的对象,相当于Java类中的关键字this。page对象在开发中几乎不用,了解一下即可。

⑨、pageContext对象

隐藏对象pageContext为javax.servlet.jsp.PageContext类的实例。pageContext对象代表当前JSP页面编译后的内容。通过pageContext能够获取到JSP中的资源,一般用它来获取其它八个内置对象。pageContext常用方法如下:

  1. JspWriter getOut()返回out对象
  2. HttpSession getSession()返回Session对象(session)
  3. Object getPage()返回page对象
  4. ServletRequest getRequest():返回request对象
  5. ServletResponse getResponse()返回response对象
  6. void setAttribute(String name,Object attribute)设置属性及属性值 ,在page范围内有效
  7. void setAttribute(String name,Object obj,int scope)在指定范围内设置属性及属性值 ,int1=page,2=request,3=session,4=application
  8. public Object getAttribute(String name)取属性的值
  9. Object getAttribute(String name,int scope)在指定范围内取属性的值
  10. public Object findAttribute(String name):寻找一属性,返回起属性值或NULL
  11. void removeAttribute(String name)删除某属性
  12. void removeAttribute(String name,int scope)在指定范围删除某属性
  13. int getAttributeScope(String name):返回某属性的作用范围
  14. Enumeration getAttributeNamesInScope(int scope):返回指定范围内可用的属性名枚举
  15. void release():释放pageContext所占用的资源
  16. void forward(String relativeUrlPath):使当前页面重导到另一页面
  17. void include(String relativeUrlPath):在当前位置包含另一文件

4.5 JSP的4种作用域

JSP的4种作用域分别为:page域、request域、session域、application域。所谓的作用域就是通过setAttribute()设置一个属性之后,可以经过多少个其它页面后仍然可以通过getAttribute()获取到值的保存范围。所以我们在实际使用中,一定要区分内置对象的作用域。

域对象中操作共享数据的方法:

  • void setAttribute(String attributeName, Object attributeValue):在域对象中共享数据
  • Object getAttribute(String attributeName):获取域对象中共享的数据
  • void removeAttribute(String attributeName):删除域对象中共享的数据
域对象 作用域范围 使用场景
page域 只在当前页面有效 jstl中大部分的标签都会默认将数据共享在pageContext中
request域 只在当前请求中有效。(也就是一次请求中有效,第二次请求就不能再获取到了) 如果客户向服务器发请求,产生的数据,用户看完就没用了,像这样的数据就存在request域,像错误信息提示,查询所有数据展示在页面中,新闻数据,属于用户看完就没用的。
session域 在当前会话中有效,仅供单个用户使用。(也就是在此次打开的浏览器中有效,如果关闭浏览器再打开就失效了) 如果客户向服务器发请求,产生的数据,用户用完了等一会儿还有用,像这样的数据就存在session域中,像购物数据,用户需要看到自己购物信息,并且等一会儿,还要用这个购物数据结帐;还有记录用户的登录状态。
application域 在整个服务器中保存数据,全部用户共享。(重启服务器后失效) 如果客户向服务器发请求,产生的数据,用户用完了,还要给其它用户用,像这样的数据就存在application(servletContext)域中,像聊天数据。

4种作用域小结:

  • 转发可以访问请求域中的数据,重定向不可以访问请求域中的数据(因为转发是一次请求,而重定向是两次请求)
  • session域对象中的数据区分浏览器
  • session域对象中的数据跟服务器是否关闭没有关系,只跟浏览器是否关闭有关
  • application域对象中的数据跟浏览器是否关闭没有关系,只跟服务器是否关闭有关

4.6 JSP的原理

我们知道,jsp的本质就是一个servlet,你访问的每一个jsp页面最后其实都是访问了一个Servlet,我们随便找一个JSP页面(注意:如果Tomcat没有配置全局变量的话,Tomcat编译jsp产生的java和class文件默认存储在C盘tomcat的work目录下)

image

然后打开index_jsp.java文件查看:

package org.apache.jsp;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;

public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase
    implements org.apache.jasper.runtime.JspSourceDependent,
org.apache.jasper.runtime.JspSourceImports {

    private static final javax.servlet.jsp.JspFactory _jspxFactory =
        javax.servlet.jsp.JspFactory.getDefaultFactory();

    private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants;

    private static final java.util.Set<java.lang.String> _jspx_imports_packages;

    private static final java.util.Set<java.lang.String> _jspx_imports_classes;

    static {
        _jspx_imports_packages = new java.util.HashSet<>();
        _jspx_imports_packages.add("javax.servlet");
        _jspx_imports_packages.add("javax.servlet.http");
        _jspx_imports_packages.add("javax.servlet.jsp");
        _jspx_imports_classes = null;
    }

    private volatile javax.el.ExpressionFactory _el_expressionfactory;
    private volatile org.apache.tomcat.InstanceManager _jsp_instancemanager;

    public java.util.Map<java.lang.String,java.lang.Long> getDependants() {
        return _jspx_dependants;
    }

    public java.util.Set<java.lang.String> getPackageImports() {
        return _jspx_imports_packages;
    }

    public java.util.Set<java.lang.String> getClassImports() {
        return _jspx_imports_classes;
    }

    public javax.el.ExpressionFactory _jsp_getExpressionFactory() {
        if (_el_expressionfactory == null) {
            synchronized (this) {
                if (_el_expressionfactory == null) {
                    _el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory();
                }
            }
        }
        return _el_expressionfactory;
    }

    public org.apache.tomcat.InstanceManager _jsp_getInstanceManager() {
        if (_jsp_instancemanager == null) {
            synchronized (this) {
                if (_jsp_instancemanager == null) {
                    _jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig());
                }
            }
        }
        return _jsp_instancemanager;
    }

    public void _jspInit() {
    }

    public void _jspDestroy() {
    }

    public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
        throws java.io.IOException, javax.servlet.ServletException {

        final java.lang.String _jspx_method = request.getMethod();
        if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method) && !javax.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {
            response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSPs only permit GET, POST or HEAD. Jasper also permits OPTIONS");
            return;
        }

        final javax.servlet.jsp.PageContext pageContext;
        javax.servlet.http.HttpSession session = null;
        final javax.servlet.ServletContext application;
        final javax.servlet.ServletConfig config;
        javax.servlet.jsp.JspWriter out = null;
        final java.lang.Object page = this;
        javax.servlet.jsp.JspWriter _jspx_out = null;
        javax.servlet.jsp.PageContext _jspx_page_context = null;


        try {
            response.setContentType("text/html;charset=UTF-8");
            pageContext = _jspxFactory.getPageContext(this, request, response,
                                                      null, true, 8192, true);
            _jspx_page_context = pageContext;
            application = pageContext.getServletContext();
            config = pageContext.getServletConfig();
            session = pageContext.getSession();
            out = pageContext.getOut();
            _jspx_out = out;

            out.write("\n");
            out.write("<html>\n");
            out.write("  <head>\n");
            out.write("    <title>Hello</title>\n");
            out.write("  </head>\n");
            out.write("\n");
            out.write("  <body>\n");
            out.write("    Hell Word!\n");
            out.write("    Hello Servlet!\n");
            out.write("    Hello JSP!\n");
            out.write("  </body>\n");
            out.write("</html>\n");
        } catch (java.lang.Throwable t) {
            if (!(t instanceof javax.servlet.jsp.SkipPageException)){
                out = _jspx_out;
                if (out != null && out.getBufferSize() != 0)
                    try {
                        if (response.isCommitted()) {
                            out.flush();
                        } else {
                            out.clearBuffer();
                        }
                    } catch (java.io.IOException e) {}
                if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
                else throw new ServletException(t);
            }
        } finally {
            _jspxFactory.releasePageContext(_jspx_page_context);
        }
    }
}

从上面可以看出,index.jsp在运行被访问后解析成了一个Java类index_jsp.java,所生成的servlet的名字默认为jsp文件名加_jsp.java,例如:index.jsp变为index_jsp.java;test.jsp变为test_jsp.java。jsp所翻译成的servlet继承与于org.apache.jasper.runtime.HttpJspBase基类,而HttpJspBase又继承了HttpServlet类,所以说JSP本身就是一种Servlet。

经过观察,HttpJspBase中对HttpServlet继承的service(HttpServletRequest,HttpServletResponse)进行了重写,在重写的方法中调用了_jspService(HttpServletRequest,HttpServletResponse),因此在jsp所翻译成的servlet中真正处理请求和响应的就是_jspService(HttpServletRequest,HttpServletResponse)

在jsp所翻译成的servlet的_jspService()中,将HTML代码通过out.write()响应到浏览器,将jsp脚本片段中的代码直接在_jspService()执行,jsp表达式中的内容会通过out.print()响应到浏览器,jsp声明会在jsp所翻译成的servlet中直接声明相应的成员变量。

5、EL和JSTL表达式的使用

5.1 EL表达式介绍

EL的全称为Expression Language,即表达式语言,它是JSP内置的表达式语言。EL的用法比较的简单,就是用 **\({ 标识符 }** 包起来的语句。我们一般用它来**读取数据**【如:`\){username} 】或者 **执行运算**【也就是基本的关系运算、逻辑运算和算术运算如:\({username==null}`】。使用EL表达式最大的特点就是使用很方便,例如获取用户名 `\){username}` 。

EL表达式的特点:

  • 可以自动对获取的数据进行数据类型的转换
  • 若通过EL获取的数据为null,则在页面中什么都不显示
  • EL一般使用访问属性的方式访问数据

下面对比一下在JSP中获取该属性,如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>EL表达式的使用</title>
  </head>
  <body>
  <%
      //在session中设置值
      session.setAttribute("username","唐浩荣");
      //JSP方式获取session中的值
      String username = (String) session.getAttribute("username");
      //方式一
      out.println("JSP方式一获取:"+username);
  %>
  <br>
  <%--方式二--%>
  <%= "JSP方式二获取:"+ username %>

  <hr>
  <%--EL方式获取session中的值--%>
  EL方式获取:${username}

  </body>
</html>

运行结果如下:

image

两者相比,可以发现我们之前在JSP中写Java代码必须写在<% %>里面,并且取值代码比较繁琐。而EL表达式可以使我们的取值代码更加简洁方便。

EL表达式除了提供上面这种方式获取数据之外,还提供了其它两种运算符来存取数据:

  • 对象 . 属性
  • 对象[ "属性名" ]

其中,对象[ "属性名" ] 可以访问集合或者是数组的元素、Bean的属性但是必须保证需要取得对象的那个属性有相对应的setXxx和getXxx方法才行,因为它是根据setXxx和getXxx方法来获取的,你可以【Ctrl+鼠标左键】选中属性试一试。下面是这两种EL表达式的使用举例:

①、对象 . 属性的使用:

${sessionScope.user.username}
//等价于
${sessionScope.user["username"]}

②、对象[ "属性名" ] 的使用(点和[ ] 可以混合使用)

${sessionScope.user[0].username}

5.2 EL表达式中的11个隐式对象

在上面看到的sessionScope是EL表达式提供的隐含对象,大家已经知道在JSP中有9个隐含对象(就是9个内置对象),而EL也有自己的隐含对象。EL隐含对象总共有11个(pageScope、requestScope、sessionScope、applicationScope、param 、paramValues、header、headerValues、initParam、cookie、pageContext),不过我们只简单看以下这四个:

EL隐含对象 作用 JSP对象
pageScope 获取pageContext域对象中的数据 pageContext
requestScope 获取request域对象中的数据 request
sessionScope 获取session域对象中的数据 session
applicationScope 获取application域对象中的数据 application

在EL表达式从这四个域中获得某个值的格式为:${xxxScope.key};等价于<%xxx.getAttribute(“username”)%>。而如果我们不指定作用域,直接${username}的话,默认会按照作用域的范围从小到大(page->request->session->application)依次查找,直到找到application为止。如果四个作用域中都没有则返回空字符串(注意:不是null,而是" "),然后什么都不显示。

补充:param和paramValues

①、param的作用:获取请求中的参数,格式:${param.请求参数名},等价于:request.getParameter("请求参数名")

②、paramValues的作用:获取多个同名的请求参数,格式:${paramValues.请求参数名},等价于request.getParameterValues("请求参数名"),例如:

<!--getContextPath():获取web应用的名称 如:/servlet01-->
<a href="${pageContext.request.contextPath}/testParam.jsp?username=admin&hobby=a&hobby=b">testParam.jsp</a>

username:${param.username}<br>
hobby:${paramValues.hobby[0]},${paramValues.hobby[1]}

5.2 EL表达式的运算

EL的运算符和Java中的运算符基本一致,优先级也相同,但是加号(+)运算符不再是连接了,而是真正的加法运算。

①、四则运算符

四则运算符 说明 举例 结果
+ ${5+5} 10
- ${5-5} 0
* ${5*5} 25
÷ ${5÷5} 1.0

②、关系运算符

关系运算符 说明 举例 结果
== 或 eq 等于 ${5==5}或${5 eq 5} true
!= 或 ne 不等于 ${5!=5}或${5 ne 5} false
< 或 lt 小于 ${3<5}或${3 lt 5} true
> 或 gt 大于 ${3>5}或${3 gt 5} false
<= 或 le 小于等于 ${3<=5}或${3 le 5} true
>= 或 ge 大于等于 ${3>=5}或${3 ge 5} false

③、逻辑运算符

逻辑运算符 说明 示例 结果
&& 或 and 交集 ${A && B}或${A and B} true/false
|| 或 or 并集 ${A || B}或${A orB}
! 或 not ${!A}或${not A} true/false

④、empty运算符

该运算用来来检查对象是否为null(空),例如:${empty A}或者${!empty A}

在默认使用empty(不是!empty)的情况下:

  • 如果A为null时,返回true,否则返回false。
  • 如果A不存在时,返回true,否则返回false。
  • 如果A为空字符串时,返回true,否则返回false。
  • 如果A为空数组时,返回true,否则返回false。
  • 如果A为空的Map时,返回true,否则返回false。
  • 如果A为空的Collection时,返回true,否则返回false。

⑤、三元表达式

三元表达式格式为:${user!=null?user.username :" "}。如果?前面结果为true则选择前面的数据,否则选择后面的。

5.3 JSTL表达式介绍

JSTL,JSP标准标签库(JavaSeverPages Standard Tag Libary)的使用是为弥补HTML标签的不足,规范自定义标签的使用而诞生的。使用JSLT标签的目的就是不希望在JSP页面中出现Java逻辑代码。所以说它和EL一样,也是为了简化我们的JSP代码。JSTL一般与EL表达式结合使用,其中EL表达式多用于取值操作,而JSTL则可以方便我们对集合进行遍历,对数据进行判断等操作。

JSTL标签库可分为5类:

  • 核心标签(用得最多)
  • 格式化标签(I18N,日期等格式化标签)
  • SQL标签(SQL标签,很少使用)
  • XML标签(几乎不用)
  • JSTL函数(EL函数)

JSTL的核心标签库标签共14个,这些标签能够完成JSP页面的基本功能,减少编码工作。从功能上可以分为4类:表达式控制标签、流程控制标签、循环标签、URL操作标签。

  1. 表达式控制标签:out标签、set标签、remove标签、catch标签。
  2. 流程控制标签:if标签、choose标签、when标签、otherwise标签。
  3. 循环标签:forEach标签、forTokens标签。
  4. URL操作标签:import标签、url标签、redirect标签、param标签。

GIF 2020-4-29 21-35-18

首先要使用JSTL就得先下载并且导入相应的包。首先打开这个网站:Standard Taglib。然后选择最新版的JSTL,点击download

image

可以发现目前最新版本是1.2.5,将下面前两个jar包下载后导入到项目中即可。

不会的可以参考这篇博客:IDEA创建一个JavaWeb项目

image

注意:我们必须要把下载的前两个包拷贝粘贴进WEB-INF下的lib目录中,通过Project Structure(Idea的操作)确保jar包已经引入,否则我们JSTL标签会报错,甚至是JSP访问不了或者报500报错。

然后我们在JSP页面添加标签的引用就可以使用JSTL标签了。引入JSTL的核心标签库如下:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
  • uri:指定引入的标签库的路径
  • prefix:设置标签库中标签的前缀,解决标签重名的问题

当然此时也可以发现其他的标签库:

image

引入格式如下所示:

<%--JSTL 核心标签--%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%--JSTL 格式化标签--%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%--JSTL SQL标签--%>
<%@ taglib prefix="sql" uri="http://java.sun.com/jsp/jstl/sql" %>
<%--JSTL XMLb标签--%>
<%@ taglib prefix="x" uri="http://java.sun.com/jsp/jstl/xml" %>
<%--JSTL 函数标签--%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>

由于核心标签是使用最多的,所以只介绍核心标签库的使用,而且只介绍常用的。

5.4 JSTL的核心标签库

5.4.1 普通标签

①、<c:out>标签:用于将结果输出。类似于JSP中的<%= %>表达式,或者是EL表达式${expression }

<%--取值--%>
<c:out value="${userName}" default="zhangsan"></c:out>

内部属性介绍:

  • value:设置要输出到页面的数据,要使用EL表达式
  • default:设置一个默认值,当value所设置的EL表达式的值为null时,则将default所对应的值输出到页面
  • escapeXml:设置输出到页面中的内容是否转义,默认值为true,表示转义,即原样输出;若设置为false,表示不转义,输出到页面中的内容会被浏览器解析

②、<c:set>标签:用于把某一个对象存在指定的域范围内,或者将某一个对象存储到Map或者JavaBean对象中。

<%--设值--%>
<c:set var="userName" value="tanghaorong" scope="session"></c:set>

内部属性介绍:

属性 描述 是否必须 缺省值
var 设置共享数据的属性名
value 设置共享数据的属性值
scope 设置共享数据的域对象的范围,若不指定域对象的范围,则默认在pageContext域对象中共享数据,scope="page(默认)` request session
target 要修改属性的变量名,一般为javabean对象
property 需要修改的javabean属性

注意:如果指定了target属性, 那么property属性也必须指定。

③、<c:remove>标签:相较于<c:set>,作用是移除范围域中的变量。

<%--删除userName变量--%>
<c:remove var="userName"></c:remove>

内部属性介绍:

  • var:要删除的域对象中共享数据的属性名
  • scope:要删除共享数据的域对象的范围,
  • scope="page|request|session|application",若不指定scope,则删除所有域对象中以var的值作为属性名的数据

④、<c:catch>标签:用于捕获在嵌套在标签体中的内容抛出的异常,并将异常信息保存到变量中。

<c:catch var="exception">
  int i=5/0;
</c:catch>

<c:out value="${exception}"/>
<%--此句相当于exception.getMessage--%>
<c:out value="${exception.message}"/>

如果嵌套的代码出现异常,异常对象会被捕获,并且保持到var变量中,该变量总是有page范围,如果没有发生异常,则var所表示的范围变量将移除。而如果没有指定var属性,异常只是被简单捕获,但异常信息不会被保存。

5.4.2 条件标签(重要)

①、<c:if>标签:和程序中的if语句作用相同,用来实现条件控制,但是没else功能。

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE HTML>
<html>
<head>
    <title>JSTL的if标签示例</title>
</head>
<body>
<%
    //在session中设置值
    session.setAttribute("score",70);
%>

<c:if test="${score>60}">
    <span>恭喜!及格了</span>
</c:if>

</body>
</html>

若为条件为true,则打印中间的数据,当然我们也可以使用var变量来声明。

<c:if test="testCondition" [var="varName"] [scope="{page|request|session|application}"]>
       标签体内容
</c:if>

其中参数说明:

  1. test属性用于存放判断的条件,一般使用EL表达式来编写。
  2. var属性用来存放判断的结果,类型为true或false。
  3. scopes属性用来指定var属性存放的范围。

②、<c:choose><c:when><c:otherwise>标签:这三个标签通常一起使用,<c:choose>标签作为<c:when><c:otherwise>标签的父标签来使用。类似于程序中if-else,其中<c:when>表示if,<c:otherwise>表else。

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE HTML>
<html>
<head>
    <title>JSTL的if-else标签示例</title>
</head>
<body>
<%
    //在session中设置值
    session.setAttribute("score",50);
%>

<c:choose>
    <c:when test="${score>60}">
        <span>恭喜!及格了</span>
    </c:when>
    <c:otherwise>
        <span>不好意思!不及格</span>
    </c:otherwise>
</c:choose>

</body>
</html>

5.4.3 循环标签(非常重要)

①、<c:forEach>标签:该标签根据循环条件遍历集合(Collection)中的元素。

<c:forEach>标签的语法:

 <c:forEach
      var=”name”
      items=”Collection”
      [varStatus=”StatusName”]
      [begin=”begin”]
      [end=”end”]
      [step=”step”]>

    主体内容

</c:forEach>

其中参数说明:

属 性 描 述 是否必须 缺省值
var 设定变量名用于存储从集合中取出元素
items 指定要遍历的集合
varStatus 设定变量名,该变量用于存放集合中元素的信息。
begin 指定遍历的起始位置 0
end 指定遍历的终止位置 最后一项
step 步长 1

值得一提的是,其中varStatus包括以下属性:

特性 描述
current 当前这次迭代的(集合中的)项
index 当前迭代的迭代索引 (从 0 开始)
count 当前这次迭代从 1 开始的迭代计数
first 用来表明当前这轮迭代是否为第一次迭代的标志
last 用来表明当前这轮迭代是否为最后一次迭代的标志
begin begin 属性值
end end 属性值
step step 属性值

forEach标签迭代集合举例(需要创建一个User,其中就两个属性,userName和age):

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ page import="com.thr.User" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE HTML>
<html>
<head>
    <title>JSTL的迭代标签示例</title>
</head>
<body>
<%
    List<User> users = new ArrayList<>();
    users.add(0,new User("张三",20));
    users.add(1,new User("李四",21));
    users.add(2,new User("王五",22));
    users.add(3,new User("赵六",23));
    users.add(4,new User("孙七",24));
    users.add(5,new User("周八",25));
    users.add(6,new User("吴九",26));
    users.add(7,new User("郑十",27));
    //在session中设置值
    session.setAttribute("users",users);
%>

<c:out value="输出整个迭代的信息:"/><br>
<c:forEach var="user" items="${users}" varStatus="userS">
    <span>姓名:${user.userName}--年龄:${user.age}</span><br>
</c:forEach>
<br><br><br><br>

<c:out value="输出其它属性信息:"/><br>
<c:forEach var="user" items="${users}" varStatus="userStatus" begin="4" end="6" step="1">
    <hr>
    步长:${userStatus.step} <br>
    开始位置: ${userStatus.begin} <br>
    结束位置:${userStatus.end} <br>
    
    下标:${userStatus.index} <br>
    计数:${userStatus.count} <br>
    是否是第一个:${userStatus.first} <br>
    是否是最后一个:${userStatus.last}<br>
</c:forEach>

</body>
</html>

下面示例通过<c:forEach><c:if>标签,实现电影信息的显示,并且实现表格斑马条纹的样式效果。

<table>
    <tr><th>分类编号</th><th>分类名称</th></tr>
    <c:forEach var="c" items="${categories}" varStatus="vs">
        <c:if test="${vs.index%2==0}">
            <tr style="background-color:yellow;"><td>${c.id}</td><td>${c.name}</td>
        </c:if>
        <c:if test="${vs.index%2!=0}">
            <tr><td>${c.id}</td><td>${c.name}</td>
        </c:if>
    </c:forEach>
</table>

②、<c:forTokens>标签:该标签用于浏览字符串,并根据指定的字符将字符串截取。

<c:forTokens>标签的语法:

<c:forTokens items=”strigOfTokens”
             delims=”delimiters”
            [var=”name”]
            [begin=”begin”]
            [end=”end”]
            [step=”len”]
            [varStatus=”statusName”] >

    主体内容

</c:forTokens>

其中参数说明:

  • items指定被迭代的字符串。
  • delims指定使用的分隔符。
  • var指定用来存放遍历到的成员。
  • begin指定遍历的开始位置(int型从取值0开始)。
  • end指定遍历结束的位置(int型,默认集合中最后一个元素)。
  • step遍历的步长(大于0的整型)。
  • varStatus存放遍历到的成员的状态信息。

forTokens使用范例:

<%@ page language="java" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE HTML>
<html>
<head>
    <title>JSTL的forTokens标签实例</title>
</head>

<body>

<c:out value="根据一个分隔符截取:"/><hr>
<c:forTokens items="123-4567-8910" delims="-" var="tel">
    <c:out value="${tel}"></c:out><br/>
</c:forTokens>

<br/><br/><br/>

<c:out value="根据多个分隔符截取:"/><hr>
<c:forTokens var="str" items="太阳、星星、月亮;地球|火星" delims="、;|">
    <c:out value="${str}"></c:out><br/>
</c:forTokens>

<br/><br/><br/>

<c:out value="输出其它属性信息:"/><br><hr>
<c:forTokens items="1*2*3*4*5*6*7"
             delims="*"
             begin="1"
             end="3"
             var="n"
             varStatus="s">

    数字 <c:out value="${n}"/> 的四种属性:<br>
    所在位置,即索引:<c:out value="${s.index}"/><br>
    总共已迭代的次数:<c:out value="${s.count}"/><br>
    是否为第一个位置:<c:out value="${s.first}"/><br>
    是否为最后一个位置:<c:out value="${s.last}"/><br>
    <hr>
</c:forTokens>
</body>
</html>

注意:如果未设定截取分隔符(即delims属性未设置值)或在字符串中没有找到匹配的分隔符,那么在显示的时候会除去匹配的分隔符,而未匹配的则会继续在页面中显示。

5.4.4 格式化标签

<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>

日期格式化标签:<fmt:formatDate>

属性名 说明 EL 类型 必须 默认值
value 将要格式化的日期对象。 Java.util.Date
type 显示的部分(日期 date、时间 time或者两者 both)。 String date
partten 格式化的样式。 String

6、Filter(过滤器)的使用

6.1 Filter简介

Filter称之为过滤器。我们生活中的过滤器有过滤网、净水器、空气净化器等。而Web中过滤器是用来对Web服务器管理的Web资源进行拦截(如JSP, Servlet, 静态图片文件或静态html文件等),从而完成一些特殊的功能比如:实现URL级别的权限访问控制(最常用)、字符编码、登录限制、过滤敏感词汇、文件压缩,跨域设置等一些高级功能。

在Servlet API中提供了一个Filter接口,开发web应用时,如果编写的Java类实现了这个接口,则把这个java类称之为过滤器Filter。通过Filter技术,开发人员可以实现用户在访问某个目标资源之前,对访问的请求和响应进行拦截,如下所示:

image

上面图片的理解就是:当一个请求过来时,首先会被过滤器拦截下来,若满足条件,则进入下一步,执行完毕之后,又返回Filter,最后再返回给客户端。若不满足条件,则直接返回。

6.2 Filter开发步骤

Filter开发步骤很简单,分为两个步骤:

  1. 编写Java类实现Filter接口,并实现其doFilter方法(也以重写init方法与destroy方法)。
  2. 在web.xml 文件中使用<filter><filter-mapping>元素配置filter,并设置它所能拦截的资源。当然我们也可以通过注解来实现。

下面举个简单的例子来说明下:字符编码拦截。我还记的在之前的博客中,如果要把含有中文的数据输出到网页中而不出现乱码的话,就必须在代码中下面这些信息。

request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html");

如果页面不多的话还好,如果页面多了的话,就要写很多重复。而用过滤器只需写一遍即可。

使用Filter代码示例:

package com.thr;

import javax.servlet.*;
import java.io.IOException;

/**
 * @author tanghaorong
 * @date 2020-05-02
 * @desc  创建一个实现Filter的实现类
 */
public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter初始化,只初始化一次...");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {

        Object user = session.getAttribute("user");

        //对request和response进行一些预处理
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");

        // 如果等于 null,说明还没有登录
        if (user == null) {
            servletRequest.getRequestDispatcher("/login.jsp").forward(servletRequest,servletResponse);
            return;
        }else {
            System.out.println("拦截前执行...");
            //交给下一个过滤器或servlet处理 让程序继续往下访问用户的目标资源
            filterChain.doFilter(request,response);
            System.out.println("拦截后执行...");
        }
    }
    @Override
    public void destroy() {
        System.out.println("Filter销毁...");
    }
}

在web. xml中配置过滤器:

<?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">
    <!--配置过滤器-->
    <filter>
        <filter-name>MyFilter</filter-name>
        <filter-class>com.thr.MyFilter</filter-class>
    </filter>
    <!--映射过滤器-->
    <filter-mapping>
        <filter-name>MyFilter</filter-name>
        <!--/* :表示拦截所有的请求 -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

拦截的规则和Servlet的匹配规则一样,它们的拦截规则如下:

  • 以指定资源匹配。例如:/index.jsp、/login
  • 以目录匹配。例如:/servlet/* 、 /abc/def
  • 以后缀名匹配,例如:*.jsp 、 *.do
  • 通配符,拦截所有web资源:/*

注意的点也是一样的:/abc/.do、/.do、abc*.do 这些都是非法的,这样的拦截规则是无效的。

我们也可以为一个过滤器配置多个映射,也就是一个Filter对应多个。但是这样设计好像没什么用,因为只要有一个URL匹配就会被拦截。

上面是对于单个Filter配置多个mapping的情况,而后面还有多个Filter对应多个mapping,这种多个Filter组合起来就称之为一个Filter链,此时的Filter的执行顺序是不确定的。因为这个地方难以理解,所以后面会详细介绍。

6.3 Filter的注解配置

当然我们也可以不在web.xml中配置,可以使用注解——@WebFilter。@WebFilter 用于将一个类声明为过滤器,该注解将会在部署时被容器处理,容器将根据具体的属性配置将相应的类部署为过滤器。该注解包含的所有属性如下:

属性名 类型 描述
filterName String 指定过滤器的 name 属性,等价于<filter-name>
value String[] 该属性等价于 urlPatterns 属性。但是两者不应该同时使用。
urlPatterns String[] 指定一组过滤器的 URL 匹配模式。等价于<url-pattern> 标签。
servletNames String[] 指定过滤器将应用于哪些 Servlet。取值是 @WebServlet 中的 name 属性的取值,或者是 web.xml 中<servlet-name> 的取值。
dispatcherTypes DispatcherType 指定过滤器的转发模式。具体取值包括:ASYNC、ERROR、FORWARD、INCLUDE、REQUEST。
initParams WebInitParam[] 指定一组过滤器初始化参数,等价于<init-param>标签。
asyncSupported boolean 声明过滤器是否支持异步操作模式,等价于<async-supported>标签。
description String 该过滤器的描述信息,等价于 <description> 标签。
displayName String 该过滤器的显示名,通常配合工具使用,等价于<display-name>标签。
smallIcon String 此Filter的小图标。
largeIcon String 此Filter的大图标。

使用@WebFilter的一个简单举例,还是用上面MyFilter过滤器。不过在测试的发现了一个问题,就是使用了注解配置Filter,但是继续在web.xml文件中配置该Filter(配置信息一样),好像是不会报错的。这个我也不知道是怎么回事,每种方式我都去试了一下,结果不会出现问题,可能过滤器是支持这种方式的吧,但是Servlet就不行会报错。(如果有大神知道可以多多指出,毕竟我还是个菜鸟)

/**
 * @author tanghaorong
 * @date 2020-05-03
 * @desc  使用注解配置Filter
 */
@WebFilter(filterName = "myFilter",value = "/*")
public class MyFilter implements Filter {

    code...

}

上面使用注解和web.xml同时配置不报错的原因是:服务器在启动时首先是会加载web.xml的文件,Filter的映射据<filter-mapping>的顺序来映射的,由于上面的代码使用注解和web.xml同时配置了同一个Filter,相当于配置两次<filter><filter-mapping>。故这个Filter会初始化两次,由于先加载的web.xml文件,所以当@WebFilter设置的过滤器被初始化时,它得到的字符串为空。

6.4 Filter的生命周期

在Filter接口中定义了三个方法,这三个都是与Filter的生命相关的方法,我们看一下源码:

package javax.servlet;

import java.io.IOException;

public interface Filter {
    void init(FilterConfig var1) throws ServletException;

    void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;

    void destroy();
}

对于这三个方法的介绍:

  1. init(FilterConfig var1):表示Filter对象的初始化方法,在Filter对象创建时执行(只执行一次),并且传入一个FilterConfig类型的参数,该参数封装了Filter的<init-param>初始化参数。
  2. doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3):表示Filter执行过滤的核心方法,如果某资源在已经被配置到这个Filter进行过滤的话,那么每次访问这个资源都会执行doFilter方法,注意:这里的request和response没有http需要强制转换。
  3. destory():表示Filter销毁方法,当Filter对象销毁时执行该方法(仅执行一次)。

6.5 多个过滤器的执行顺序

如果在一个Web应用中,编写了多个Filter,那么这些Filter组合起来称之为一个Filter链

  • 此时的Web服务器根据Filter在web.xml文件中的<filter-mapping>定义顺序执行。
  • 如果是注解的话则根据类名先后顺序执行。
  • 如果web.xml和注解混合使用,则先加载web.xml然后注解。

当第一个Filter的doFilter方法被调用时,Web服务器会创建一个代表Filter链的FilterChain对象传递给该方法。在doFilter方法中,开发人员如果调用了FilterChain对象的doFilter方法,则Web服务器会检查FilterChain对象中是否还有Filter,如果还有,则调用第2个Filter,依次类推,直到没有可以调用目标资源。

image-20220817153441211

image

举个例子好好理解一下。分别创建A、B、C、D 四个Filter。其中A和D过滤器用注解配置,B和C过滤器用web.xml配置。

AFilter的代码示例如下:(因为DFilter代码大致相同,所以我就贴一个,就类名和打印时的名称不一样罢了。如果使用web.xml配置就把注解注释掉即可)

/**
 * @author tanghaorong
 * @date 2020-05-03
 * @desc 使用注解配置AFilter
 */
@WebFilter(filterName = "AFilter",value = "/*")
public class AFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("初始过滤器名称:AFilter");
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("AFilter拦截前执行...");
        filterChain.doFilter(request,response);
        System.out.println("AFilter拦截后执行...");
    }
    @Override
    public void destroy() {
        System.out.println("AFilter销毁...");
    }
}

BFilter和CFilter在web.xml的配置如下:(注意:执行顺序跟<filter>的顺序无关。而是根据<filter-mapping>的顺序决定执行顺序)

<?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">
    <!--配置过滤器-->
    <filter>
        <filter-name>CFilter</filter-name>
        <filter-class>com.thr.CFilter</filter-class>
    </filter>
    <filter>
        <filter-name>BFilter</filter-name>
        <filter-class>com.thr.BFilter</filter-class>
    </filter>
    <!--映射过滤器-->
    <!--注意:这里CFilter在BFilter之前-->
    <filter-mapping>
        <filter-name>CFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>BFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

运行后的结果如下:

①、这是初始化的顺序截图:

image

每次运行的结果都是这个,不知道过滤器初始化创建的顺序根据什么来的执行的?不过这不是重点,重点在下面。

②、这是过滤器运行顺序截图:

image

可以看到,过滤器的执行结果符合我们的预期。CFilter和BFilter是在web.xml中声明的,且CFilter的定义在BFilter之前,所以CFilter在BFilter之前执行。而注解则是根据类来执行的。并且可以发现每当过滤器调用filterChain.doFilter(request,response)之后,检查到后面还有过滤器没有执行,则继续执行后面的过滤器,当检查到没有过滤器后,再依次执行过滤器后面的操作。

③、这是过滤器销毁顺序截图:

image

销毁的执行顺序和初始化是一样的。

我后面又试了试很多种方法,注解和web.xml混合配置,全注解配置,全web.xml配置。

可以的出一个结论:web.xml方式是根据mapping中先后顺序执行。而注解方式则是根据类名先后顺序排序的。但是Filter初始化和销毁时的顺序我也不知道是根据什么排的序,无论这么测试输出的格式都是一样的。

6.6 FilterConfig对象的使用

还记得在Servlet中有个ServletConfig,它代表的是当前Servlet的一些初始化参数。所以FilterConfig也是一样的,当我们在配置Filter的时候,使用<init-param>为Filter配置一些初始化参数后,在Filter对象被创建并且调用其init方法时,会把封装了Filter初始化参数的FilterConfig对象传递进来。因此通过FilterConfig对象的方法就可获取初始化配置信息。方法如下:

  1. String getFilterName():返回Filter的名称。
  2. String getInitParameter(String name): 返回在部署描述中指定名称的初始化参数的值。如果不存在返回null。
  3. Enumeration getInitParameterNames():返回过滤器的所有初始化参数的名字的枚举集合。
  4. ServletContext getServletContext():返回Servlet上下文对象的引用。

使用FilterConfig获取Filter的初始化配置信息:

web.xml中配置文件:

<?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">
    <!--配置过滤器-->
    <filter>
        <filter-name>MyFilter</filter-name>
        <filter-class>com.thr.MyFilter</filter-class>
        <!--配置初始化信息-->
        <init-param>
            <param-name>name</param-name>
            <param-value>tanghaorong</param-value>
        </init-param>
        <init-param>
            <param-name>password</param-name>
            <param-value>123456</param-value>
        </init-param>
    </filter>
    <!--映射过滤器-->
    <filter-mapping>
        <filter-name>MyFilter</filter-name>
        <!--/* :表示拦截所有的请求 -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

FilterConfig中的代码:

package com.thr;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
import java.util.Enumeration;

/**
 * @author tanghaorong
 * @date 2020-05-03
 * @desc  FilterConfig获取Filter初始化配置信息
 */
public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter初始化,只初始化一次...");

        //获取过滤器的名字
        String filterName = filterConfig.getFilterName();
        //获取在web.xml文件中配置的初始化参数
        String initParam1 = filterConfig.getInitParameter("name");
        String initParam2 = filterConfig.getInitParameter("password");

        System.out.println(filterName);
        System.out.println(initParam1);
        System.out.println(initParam2);

        //返回过滤器的所有初始化参数的名字的枚举集合。
        Enumeration<String> initParameterNames = filterConfig.getInitParameterNames();
        while (initParameterNames.hasMoreElements()) {
            String name = (String) initParameterNames.nextElement();
            String value = filterConfig.getInitParameter(name);
            System.out.println(name + "=" + value );
        }
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {

        System.out.println("拦截前执行...");
        filterChain.doFilter(request,response);
        System.out.println("拦截后执行...");
    }

    @Override
    public void destroy() {
        System.out.println("Filter销毁...");
    }
}

使用注解获取:

package com.thr;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;

/**
 * @author tanghaorong
 * @date 2020-05-03
 * @desc 使用注解配置Filter初始化参数,并且用FilterConfig获取
 */
@WebFilter(filterName = "myFilter",value = "/*",initParams = {
        @WebInitParam(name = "name", value = "tanghaorong"),/*这里配置初始化的参数*/
        @WebInitParam(name = "password", value = "123456")/*相当于<init-param>*/
})
public class MyFilter implements Filter {

    //此段代码和上面是一样的
    code...

}

7、Listener(监听器)的使用

7.1 监听器的介绍

监听器(Listener)是一个专门用于对其他对象身上发生的事件或状态改变进行监听和相应处理的对象,当被监视的对象发生情况时,立即采取相应的行动。监听器是一个实现特定接口的普通Java程序,这个程序专门用于监听另一个Java对象的方法调用或属性改变,当被监听对象发生上述事件后,监听器某个方法立即被执行。

监听器的一些相关术语:事件源、事件监听器、事件对象、响应行为。(我们形象的用:狗仔监视某明星出轨 来理解一下)

术语 描述
事件源 被监听的对象(可以理解为明星)
事件监听器 监听事件源对象,事件源对象的状态的变化都会触发监听器(狗仔)
事件对象 将监听器与事件源进行绑定(明星出轨)
响应行为 监听器监听到事件源的状态变化时 所涉及的功能代码(狗仔做出的动作,发布明星出轨信息)

7.2 监听器的分类

在JavaWeb中的监听器就是监听域中对象的状态。它们监听的分别是ServletContext域、Session域、Request域这三类。

  1. 它们按监听的对象划分为三类:

    • ServletContext对象,监听器为ServletContextListener
    • HttpSession对象,监听器为HttpSessionListener
    • ServletRequest对象,监听器为ServletRequestListener
  2. 按监听的内容划分为两类:

    • 监听域对象的创建与销毁

    • 监听域对象的属性变化

整合如下图:

ServletContext域 HttpSession域 ServletRequest域
域对象的创建于销毁 ServletContextListener HttpSessionListener ServletRequestListener
域对象内的属性变化 ServletContextAttributeListener HttpSessionAttributeListener ServletRequestAttributeListener

7.3 监听ServletContext域对象的创建和销毁

ServletContextListener接口用于监听ServletContext对象的创建和销毁事件。实现了ServletContextListener接口的类都可以对ServletContext对象的创建和销毁进行监听。

当ServletContext对象被创建时,激发contextInitialized (ServletContextEvent sce)方法。

当ServletContext对象被销毁时,激发contextDestroyed(ServletContextEvent sce)方法。

ServletContext域对象创建和销毁时机:

  • 创建:服务器启动针对每一个Web应用创建ServletContext。
  • 销毁:服务器关闭前先关闭代表每一个web应用的ServletContext。

示例:编写一个MyServletContextListener类,实现ServletContextListener接口,监听ServletContext对象的创建和销毁。

监听器创建的分为三个步骤如下:

  1. 编写一个监听器类去实现监听器接口。
  2. 重写监听器的方法。
  3. 在web.xml中配置监听器。

举例代码如下所示:

①、编写监听器,并且重写监听器的方法,其代码如下:

package com.thr;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
/**
 * @author tanghaorong
 * @date 2020-05-07
 * @desc 监听ServletContext域对象的创建和销毁
 */
public class MyServletContextListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        System.out.println("ServletContext对象创建");
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
        System.out.println("ServletContext对象销毁");
    }
}

②、在web.xml文件中注册监听器。如下:

<?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">
    <!-- 注册针对ServletContext对象进行监听的监听器 -->
    <listener>
        <description>ServletContextListener监听器</description>
        <!--实现了ServletContextListener接口的监听器类 -->
        <listener-class>com.thr.MyServletContextListener</listener-class>
    </listener>
</web-app>

在web.xml中配置监听器时要注意它们的先后顺序:监听器>过滤器>Serlvet。

当然我们也可以使用注解——@WebListener。它的内部参数就一个value,所以一般直接写一个@WebListener就可以了。例如:

/**
 * @author tanghaorong
 * @date 2020-05-07
 * @desc 使用注解监听ServletContext域对象的创建和销毁
 */
@WebListener
public class MyServletContextListener implements ServletContextListener {
     
    code...
 
}

7.4 监听HttpSession域对象的创建和销毁

HttpSessionListener 接口用于监听HttpSession对象的创建和销毁。

在创建一个Session时,激发sessionCreated (HttpSessionEvent se) 方法。

在销毁一个Session时,激发sessionDestroyed (HttpSessionEvent se) 方法。

HttpSession域对象创建和销毁时机:

  • 创建:服务器第一次访问Servlet时,调用getSession()方法的时候. 如果访问的是jsp则直接创建。
  • 销毁:
    • 自动销毁:session过期了(默认30分钟);
    • 手动销毁:session.invalidate()或在xml中配置;
    • 强制销毁:非正常关闭服务器(正常关闭序列化到硬盘)。

范例:编写一个MyHttpSessionListener类,实现HttpSessionListener接口,监听HttpSession对象的创建和销毁。

①、编写监听器,这里直接使用注解配置,免得再去web.xml配置了。代码如下:

package com.thr;

import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

/**
 * @author tanghaorong
 * @date 2020-05-08
 * @desc 监听Session域对象的创建和销毁
 */
@WebListener
public class MyHttpSessionListener implements HttpSessionListener {
    @Override
    public void sessionCreated(HttpSessionEvent httpSessionEvent) {
        System.out.println(httpSessionEvent.getSession() + "创建了!!");
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
        System.out.println("session销毁了!!");
    }
}

注意:HttpSession的销毁时机需要在web.xml中进行配置,如下:

<session-config>
    <!--配置HttpSession对象的1分钟之后销毁 -->
    <session-timeout>1</session-timeout>
</session-config>

然后当我们访问jsp页面时,HttpSession对象就会创建。

运行结果如下:

image

过来一分钟之后。

image

7.5 监听ServletRequest域对象的创建和销毁

ServletRequestListener接口用于监听ServletRequest 对象的创建和销毁。

当Request对象被创建时,监听器的requestInitialized(ServletRequestEvent sre)方法将会被调用。

当Request对象被销毁时,监听器的requestDestroyed(ServletRequestEvent sre)方法将会被调用

ServletRequest域对象创建和销毁时机:

  • 创建:用户每一次访问都会创建request对象
  • 销毁:当前访问结束,request对象就会销毁

范例:编写一个MyServletRequestListener类,实现ServletRequestListener接口,监听ServletRequest对象的创建和销毁

①、编写监听器,代码如下:

package com.thr;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;

/**
 * @author tanghaorong
 * @date 2020-05-08
 * @desc 监听ServletRequest域对象的创建和销毁
 */
@WebListener
public class MyServletRequestListener implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
        System.out.println(servletRequestEvent.getServletRequest() + "销毁了!!");
    }

    @Override
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {
        System.out.println(servletRequestEvent.getServletRequest() + "创建了!!");
    }
}

测试结果如下:

image

从运行结果中可以看到,用户每一次访问都会创建request对象,当访问结束后,request对象就会销毁。

8、Servlet文件的上传与下载

8.1 前言

文件的上传与下载功能在JavaWeb中的应用是非常常见的,我们随便打开一个网站,基本上都会有上传与下载的功能,例如修改个人信息时,需要修改个人图片,这就是典型的文件上传,还有下面需要从网站中下载相关的jar包,这就是典型的文件下载,那么这个功能是怎么实现的呢?下面来学习一下。

8.2 环境搭建

由于涉及到文件的操作,所以肯定跟流有关,如果要在Servlet读取上传数据,可以通过Request对象提供的一个getInputStream方法来读取到客户端提交过来的数据(具体来说是 http 的请求体 entity)。但由于用户可能会同时上传多个文件,再在Servlet 端编程直接读取上传数据,并分别解析出相应的文件数据是一项非常麻烦的工作。

那么为了方便处理上传数据,我们一般选择采用apache的开源工具common-fileupload这个文件上传组件,该组件可以将"multipart/form-data"类型请求的各种表单域解析出来,并实现一个或多个文件上传,同时也可以限制上传文件的大小等内容,其性能十分优异,使用极其简单。而common-fileupload是依赖于common-io这个包的,所以还需要下载这个包。下载链接这里给出:

根据自己需求下载对应版本,我这里都是下载最新的版本(注意下载的是二进制文件)。

然后我们创建一个FileUploadAndDownLoad项目,将这两个jar包导入到项目的lib目录中,如下图所示:

image

接下来我们就可以编写相关代码了。

8.3 文件上传组件的核心API

fileupload组件的工作流程图如下:

image

从上图文件上传组件中有三个非常重要的类:

  • DiskFileItemFactory
  • ServletFileUpload
  • FileItem
  1. DiskFileItemFactory核心API:DiskFileItemFactory是用来创建ServletFileUpload对象的工厂,这个工厂类的常用方法:

    • public DiskFileItemFactory(int sizeThreshold, java.io.File repository) :构造函数

    • public void setSizeThreshold(int sizeThreshold):设置内存缓冲区的大小,默认值为10K。当上传文件大于缓冲区大小时, fileupload组件将使用临时文件缓存上传文件。

    • public void setRepository(java.io.File repository):指定临时文件目录,默认值为System.getProperty("java.io.tmpdir")。

  2. ServletFileUpload核心API:ServletFileUpload负责处理上传的文件数据,并将表单中每个输入项封装成一个 FileItem 对象中(每一个FileItem对应一个Form表单的输入项)。常用方法有:

    • boolean isMultipartContent(HttpServletRequest request):判断上传表单是否为multipart/form-data类型 。

    • List parseRequest(HttpServletRequest request):解析request对象,并把表单中的每一个输入项包装成一个fileItem 对象,并返回一个保存了所有FileItem的list集合。

    • setFileSizeMax(long fileSizeMax):设置上传文件的最大值 。

    • setSizeMax(long sizeMax):设置上传文件总量的最大值。

    • setHeaderEncoding(java.lang.String encoding):设置编码格式。

    • setProgressListener(ProgressListener pListener):设置监听器用来显示进度条。

  3. FileItem核心API:上面说了ServletFileUpload的作用是将表单的每个输入项封装成一个FileItem对象,那么我们就可以通过FileItem获取到封装的表单信息。常用方法有:

    • getFieldName(对于非文件上传域) :获得表单的name属性。

    • getString(对于非文件上传域):获得表单中该输入项的值(value)。

    • getName(对于文件上传域) :获得文件名。

    • getInputStream(对于文件上传域): 获得文件内容。

    • isFormField():是否为文件上传域,true不是文件上传,false是文件上传。

至此我们对这三个非常重要的类有了一定的了解,它的使用分为这四个步骤:

  1. 创建 DiskFileItemFactory 对象,设置缓冲区大小和临时文件目录。
  2. 使用 DiskFileItemFactory 对象创建 ServletFileUpload 对象,并设置上传文件的大小限制。
  3. 调用 ServletFileUpload.parseRequest() 方法解析 request 对象,得到一个保存了所有上传内容的 List 对象。
  4. 对 List 进行遍历,每遍历一个 FileItem 对象,调用其 FileItem.isFormField() 方法判断该对象是否是上传文件对象,该方法的返回值具体含义为:True 为普通表单字段,则调用 getFieldName() 获得 name 属性、getString() 方法获得 value 属性。False 为上传文件,则调用 getInputStream() 方法得到文件内容、getName() 方法获得文件名。

8.4 文件上传

在文件上传的页面中我们要特别注意两点:

  • 表单中必须将method属性设置为post,ectype属性设置为"multipart/form-data"类型。
  • input标签中必须将type属性设置为file,而且必须设置name的属性(见名知意即可),否则浏览器不会发送上传文件的数据。

①、上传页面upload.jsp的代码如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head>
        <title>文件上传</title>
    </head>
    <body>
        <!--表单的enctype属性要设置为multipart/form-data-->
        <form action="${pageContext.request.contextPath}/UploadServlet" method="post" enctype="multipart/form-data">
            <table width="600">
                <tr>
                    <td>上传者</td>
                    <td><input type="text" name="name"/></td>
                </tr>
                <tr>
                    <td>上传文件1</td>
                    <td><input type="file" name="file1"/></td>
                </tr>
                <tr>
                    <td>上传文件2</td>
                    <td><input type="file" name="file2"/></td>
                </tr>
                <tr>
                    <!--设置单元格可横跨的列数。-->
                    <td colspan="2"><input type="submit" value="上传"/></td>
                </tr>
            </table>
        </form>
    </body>
</html>

②、返回信息message.jsp的代码如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>消息提示</title>
</head>
    <body>
        ${message}
    </body>
</html>

③、创建处理上传文件的UploadServlet,代码如下:

package com.thr;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;

/**
 * @author tanghaorong
 * @date 2020-05-10
 * @desc 文件上传代码
 */
@WebServlet(name = "UploadServlet",urlPatterns = "/UploadServlet")
public class UploadServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException,IOException {
        //设置上传文件的保存目录,将上传的文件存放于WEB-INF目录下,不允许外界直接访问,保证上传文件的安全
        String savePath = this.getServletContext().getRealPath("/WEB-INF/upload");
        //打印一下,看看路径在哪
        System.out.println(savePath);
        File file = new File(savePath);
        //判断上传文件的保存目录是否存在
        if (!file.exists() && !file.isDirectory()) {
            System.out.println(savePath+"目录不存在,需要创建");
            //创建目录
            file.mkdir();
        }
        //消息提示
        String message = "";
        try{
            //使用Apache文件上传组件处理文件上传步骤:
            //1、创建一个DiskFileItemFactory工厂
            DiskFileItemFactory factory = new DiskFileItemFactory();
            //2、创建一个文件上传解析器
            ServletFileUpload upload = new ServletFileUpload(factory);
            //解决上传文件名的中文乱码
            upload.setHeaderEncoding("UTF-8");
            //3、判断提交上来的数据是否是上传表单的数据
            if(!ServletFileUpload.isMultipartContent(request)){
                //按照传统方式获取数据
                return;
            }
            //4、使用ServletFileUpload解析器解析上传数据,解析结果返回的是一个List<FileItem>集合,每一个FileItem对应一个Form表单的输入项
            List<FileItem> list = upload.parseRequest(request);
            for(FileItem item : list){
                //如果fileitem中封装的是普通输入项的数据
                if(item.isFormField()){
                    String name = item.getFieldName();
                    //解决普通输入项的数据的中文乱码问题
                    String value = item.getString("UTF-8");
                    //value = new String(value.getBytes("iso8859-1"),"UTF-8");
                    System.out.println(name + "=" + value);
                }else{//如果fileitem中封装的是上传文件
                    //得到上传的文件名称,
                    String filename = item.getName();
                    System.out.println(filename);
                    if(filename==null || filename.trim().equals("")){
                        continue;
                    }
                    //注意:不同的浏览器提交的文件名是不一样的,有些浏览器提交上来的文件名是带有路径的,如:  c:\a\b\1.txt,而有些只是单纯的文件名,如:1.txt
                    //处理获取到的上传文件的文件名的路径部分,只保留文件名部分
                    filename = filename.substring(filename.lastIndexOf("\\")+1);
                    //获取item中的上传文件的输入流
                    InputStream in = item.getInputStream();
                    //创建一个文件输出流
                    FileOutputStream out = new FileOutputStream(savePath + "\\" + filename);
                    //创建一个缓冲区
                    byte buffer[] = new byte[1024];
                    //判断输入流中的数据是否已经读完的标识
                    int len = 0;
                    //循环将输入流读入到缓冲区当中,(len=in.read(buffer))>0就表示in里面还有数据
                    while((len=in.read(buffer))>0){
                        //使用FileOutputStream输出流将缓冲区的数据写入到指定的目录(savePath + "\\" + filename)当中
                        out.write(buffer, 0, len);
                    }
                    //关闭输入流
                    in.close();
                    //关闭输出流
                    out.close();
                    //删除处理文件上传时生成的临时文件
                    item.delete();
                    message = "文件上传成功!";
                }
            }
        }catch (Exception e) {
            message= "文件上传失败!";
            e.printStackTrace();

        }
        request.setAttribute("message",message);
        request.getRequestDispatcher("/message.jsp").forward(request, response);
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        doGet(request, response);
    }
}

④、运行效果如下:

当文件上传成功之后,上传的文件保存在了WEB-INF目录下的upload目录,如下图所示:

GIF 2020-5-11 18-18-43

8.5 文件上传的细节处理

上述的代码虽然可以成功将文件上传到服务器上面的指定目录当中,但是如果我们再做一次同样的操作的话(就是将上传过的文件再上传一次),我们可以发现并不会多出来相同的图片,这是因为上传的文件名字名字是一样的,从而使的文件覆盖掉了原来的文件。而且我们也没有设置文件上传的大小,没有设置上传文件的类型,也就说可以上传任意大小的任意文件,这样就会导致出现一系列问题。综合上面所描述的,我们应该对文件的上传做出更加细节的处理,以下列出的几点需要特别注意的:

  • 乱码问题:
    • 对于表单中文乱码的处理方式:fileItem.getString("utf-8")
    • 对于上传文件名乱码处理方式:servletFileupload.setHeaderEncoding("utf-8")
  • 文件存储位置:
    • 为保证服务器安全,上传文件应该放在外界无法直接访问的目录下,比如放于WEB-INF目录下。
  • 文件名命名:
    • 上面也说了,如果上传的文件名相同的是会覆盖到原来的文件的,那么为防止文件覆盖的现象发生,要为上传文件产生一个唯一的文件名。处理方式:filename = UUID.randomUUID().toString() + filename;
  • 限制文件上传的大小 / 限制上传文件的最大值:方式为:upload.setFileSizeMax(1024*1024);表示上传的文件不能超过1MB。实现并通过捕获FileUploadBase.FileSizeLimitExceededException异常来给用户一个友好的提示。
  • 文件大小超出限制时:如果上传的文件大小超出了设置的限制大小(默认上传文件总大小是10k),那我们可以使用临时文件保存。处理方法:factory.setRepository(new File(this.getServletContext().getRealPath("/WEB-INF/temp")));

但是一般来说,文件会先保存在临时文件中,之后才会从临时文件复制到真正的存储目录中,而且这个临时文件一般不会删除,如果我们需要设置让其删除,在处理上传文件后关闭流之后调用item.delete方法即可。

  • 文件上传的多目录分散:

    当上传的文件过多时,如果不把文件分散到其它目录去的话,那么所有的文件就会集中在一个目录,这样访问某个文件需要很长时间,甚至不响应,查找困难,所以应该采用目录分散算法,有如下几种目录分散算法:

    • 按时间 —— 比如一天一个目录

    • 按用户 —— 比如淘宝某个商家上传的所有商品图片都放在一个单独目录

    • 每个目录存放固定数量文件 —— 每个目录存放1000个文件,每次上传文件时判断目录中文件是否超过1000,若超过则新建一个目录,保存在新目录中

    • 哈希目录分散算法 (推荐)—— 对于一个对象,它会有一个 32 位的 hashcode ,这个 hashcode 通过一定的哈希算法生成,允许重复。把 32 位的 hashcode 分成 4*8,每次与 1111 进行“位与”运算,得到一个 4 位的值(0~15),然后右移 4 位,继续“位与”运算,每右移一次,目录就会增加一个层级,可根据不同的业务要求决定具体的目录层级数目,对于小型项目而言,2 级目录即可满足需求(总共有(2^4)^2=256个文件夹)。具体如图所示:

      image

      对于图中数据来说,它的 1 级目录号为 12,它 2 级目录号为 14,故这个文件位于12号文件夹的14号文件夹里面。

  • 文件上传进度的监听:为了让用户有更好的体验,页面应该实时显示上传的速度、剩余时间甚至用进度条美化来代替。文件上传进度的监听器为:ProgressListener。它是一个接口,其内部有一个非常重要的方法update。

    • void update(long pBytesRead, long pContentLength, int pItems)。
    • update方法中的三个参数说明:
      • pBytesRead:表示已读取的字节总数。
      • pContentLength :表示正在读取的总字节数。可能是-1,如果这个数字是未知的。
      • pItems:表示当前正在读取的字段的编号。(0 =目前没有项,1 =第一项正在读取,…)。

我们一般直接在程序中以内部类的方式使用文件上传进度的监听器为。

//创建一个文件上传解析器
ServletFileUpload upload = new ServletFileUpload(factory);
//监听文件上传进度
upload.setProgressListener(new ProgressListener(){
    public void update(long pBytesRead, long pContentLength, int pItems) {
        System.out.println("上传文件大小为:" + pContentLength + ",当前已处理:" + pBytesRead+
                ",form第几项:" + pItems); }
});

所以针对上面的一些细节问题,对前面的UploadHandleServlet进行了改进,改进后的代码如下:

package com.thr;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadBase;
import org.apache.commons.fileupload.ProgressListener;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;

/**
 * @author tanghaorong
 * @date 2020-05-10
 * @desc 文件上传代码
 */
@WebServlet(name = "UploadServlet",urlPatterns = "/UploadServlet")
public class UploadServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        //得到上传文件的保存目录,将上传的文件存放于WEB-INF目录下,不允许外界直接访问,保证上传文件的安全
        String savePath = this.getServletContext().getRealPath("/WEB-INF/upload");
        System.out.println(savePath);
        //上传时生成的临时文件保存目录
        String tempPath = this.getServletContext().getRealPath("/WEB-INF/temp");
        File tmpFile = new File(tempPath);
        if (!tmpFile.exists()) {
            //创建临时目录
            tmpFile.mkdir();
        }

        //消息提示
        String message = "";
        try{
            //使用Apache文件上传组件处理文件上传步骤:
            //1、创建一个DiskFileItemFactory工厂
            DiskFileItemFactory factory = new DiskFileItemFactory();
            //设置工厂的缓冲区的大小,当上传的文件大小超过缓冲区的大小时,就会生成一个临时文件存放到指定的临时目录当中。
            factory.setSizeThreshold(1024*100);//设置缓冲区的大小为100KB,如果不指定,那么缓冲区的大小默认是10KB
            //设置上传时生成的临时文件的保存目录
            factory.setRepository(tmpFile);
            //创建一个文件上传解析器
            ServletFileUpload upload = new ServletFileUpload(factory);
            //监听文件上传进度
            upload.setProgressListener(new ProgressListener(){
                public void update(long pBytesRead, long pContentLength, int pItems) {
                    System.out.println("上传文件大小为:" + pContentLength + ",当前已处理:" + pBytesRead+
                            ",form第几项:" + pItems); }
            });
            //解决上传文件名的中文乱码
            upload.setHeaderEncoding("UTF-8");
            //3、判断提交上来的数据是否是上传表单的数据
            if(!ServletFileUpload.isMultipartContent(request)){
                //按照传统方式获取数据
                return;
            }

            //设置上传单个文件的大小的最大值,目前是设置为1024*1024字节,也就是1MB
            upload.setFileSizeMax(1024*1024);
            //设置上传文件总量的最大值,最大值=同时上传的多个文件的大小的最大值的和,目前设置为10MB
            upload.setSizeMax(1024*1024*10);
            //4、使用ServletFileUpload解析器解析上传数据,解析结果返回的是一个List<FileItem>集合,每一个FileItem对应一个Form表单的输入项
            List<FileItem> list = upload.parseRequest(request);
            for(FileItem item : list){
                //如果fileitem中封装的是普通输入项的数据
                if(item.isFormField()){
                    String name = item.getFieldName();
                    //解决普通输入项的数据的中文乱码问题
                    String value = item.getString("UTF-8");
                    //value = new String(value.getBytes("iso8859-1"),"UTF-8");
                    System.out.println(name + "=" + value);
                }else{//如果fileitem中封装的是上传文件
                    //得到上传的文件名称,
                    String filename = item.getName();
                    System.out.println(filename);
                    if(filename==null || filename.trim().equals("")){
                        continue;
                    }
                    //注意:不同的浏览器提交的文件名是不一样的,有些浏览器提交上来的文件名是带有路径的,如:  c:\a\b\1.txt,而有些只是单纯的文件名,如:1.txt
                    //处理获取到的上传文件的文件名的路径部分,只保留文件名部分
                    filename = filename.substring(filename.lastIndexOf("\\")+1);
                    //得到上传文件的扩展名
                    String fileExtName = filename.substring(filename.lastIndexOf(".")+1);
                    //如果需要限制上传的文件类型,那么可以通过文件的扩展名来判断上传的文件类型是否合法
                    System.out.println("上传的文件的扩展名是:"+fileExtName);
                    //获取item中的上传文件的输入流
                    InputStream in = item.getInputStream();
                    //得到文件保存的名称
                    String saveFilename = makeFileName(filename);
                    //得到文件的保存目录
                    String realSavePath = makePath(saveFilename, savePath);
                    //创建一个文件输出流
                    FileOutputStream out = new FileOutputStream(realSavePath + "\\" + saveFilename);
                    //创建一个缓冲区
                    byte buffer[] = new byte[1024];
                    //判断输入流中的数据是否已经读完的标识
                    int len = 0;
                    //循环将输入流读入到缓冲区当中,(len=in.read(buffer))>0就表示in里面还有数据
                    while((len=in.read(buffer))>0){
                        //使用FileOutputStream输出流将缓冲区的数据写入到指定的目录(savePath + "\\" + filename)当中
                        out.write(buffer, 0, len);
                    }
                    //关闭输入流
                    in.close();
                    //关闭输出流
                    out.close();
                    //删除处理文件上传时生成的临时文件
                    //item.delete();
                    message = "文件上传成功!";
                }
            }
        }catch (FileUploadBase.FileSizeLimitExceededException e) {
            e.printStackTrace();
            request.setAttribute("message", "单个文件超出最大值!!!");
            request.getRequestDispatcher("/message.jsp").forward(request, response);
            return;
        }catch (FileUploadBase.SizeLimitExceededException e) {
            e.printStackTrace();
            request.setAttribute("message", "上传文件的总的大小超出限制的最大值!!!");
            request.getRequestDispatcher("/message.jsp").forward(request, response);
            return;
        }catch (Exception e) {
            message= "文件上传失败!";
            e.printStackTrace();
        }
        request.setAttribute("message",message);
        request.getRequestDispatcher("/message.jsp").forward(request, response);
    }

    /**
     * @Method: makeFileName
     * @Description: 生成上传文件的文件名,文件名以:uuid+"_"+文件的原始名称
     * @param filename 文件的原始名称
     * @return uuid+"_"+文件的原始名称
     */
    private String makeFileName(String filename){  //2.jpg
        //为防止文件覆盖的现象发生,要为上传文件产生一个唯一的文件名
        return UUID.randomUUID().toString() + "_" + filename;
    }

    /**
     *
     * @Method: makePath
     * @Description: 为防止一个目录下面出现太多文件,要使用hash算法打散存储
     * @param filename 文件名,要根据文件名生成存储目录
     * @param savePath 文件存储路径
     * @return 新的存储目录
     */
    private String makePath(String filename,String savePath){
        //得到文件名的hashCode的值,得到的就是filename这个字符串对象在内存中的地址
        int hashcode = filename.hashCode();
        int dir1 = hashcode&0xf;  //0--15
        int dir2 = (hashcode&0xf0)>>4;  //0-15
        //构造新的保存目录
        String dir = savePath + "\\" + dir1 + "\\" + dir2;  //upload\2\3  upload\3\5
        //File既可以代表文件也可以代表目录
        File file = new File(dir);
        //如果目录不存在
        if(!file.exists()){
            //创建目录
            file.mkdirs();
        }
        return dir;
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        doGet(request, response);
    }
}

这些细节问题进行改进之后,我们的文件上传功能就算是做得比较完善了。

8.6 文件下载

文件的下载,首先我们要列出所有上传的文件,然后才能进行下载。

①、创建列出网站所有文件的ListFileServlet,代码如下:

package com.thr;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * @author tanghaorong
 * @date 2020-05-10
 * @desc 显示下载文件的页面
 */
@WebServlet(name = "ListFileServlet",value = "/ListFileServlet")
public class ListFileServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        //获取上传文件的目录
        String uploadFilePath = this.getServletContext().getRealPath("/WEB-INF/upload");
        System.out.println(uploadFilePath);
        //存储要下载的文件名
        Map<String,String> fileNameMap = new HashMap<String,String>();
        //递归遍历filepath目录下的所有文件和目录,将文件的文件名存储到map集合中
        listfile(new File(uploadFilePath),fileNameMap);//File既可以代表一个文件也可以代表一个目录
        //将Map集合发送到listfile.jsp页面进行显示
        request.setAttribute("fileNameMap", fileNameMap);
        request.getRequestDispatcher("/download.jsp").forward(request, response);
    }

    /**
     * @Method: listfile
     * @Description: 递归遍历指定目录下的所有文件
     *
     * @param file 即代表一个文件,也代表一个文件目录
     * @param map 存储文件名的Map集合
     */
    public void listfile(File file, Map<String,String> map){
        //如果file代表的不是一个文件,而是一个目录
        if(!file.isFile()){
            //列出该目录下的所有文件和目录
            File files[] = file.listFiles();
            //遍历files[]数组
            for(File f : files){
                //递归
                listfile(f,map);
            }
        }else{
            /**
             * 处理文件名,上传后的文件是以uuid_文件名的形式去重新命名的,去除文件名的uuid_部分
             file.getName().indexOf("_")检索字符串中第一次出现"_"字符的位置,如果文件名类似于:9349249849-88343-8344_阿_凡_达.avi
             那么file.getName().substring(file.getName().indexOf("_")+1)处理之后就可以得到阿_凡_达.avi部分
             */
            String realName = file.getName().substring(file.getName().indexOf("_")+1);
            //file.getName()得到的是文件的原始名称,这个名称是唯一的,因此可以作为key,realName是处理过后的名称,有可能会重复
            map.put(file.getName(), realName);
        }
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
}

②、下载页面download.jsp,代码如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <title>显示下载文件页面</title>
</head>
<body>
    <!-- 遍历Map集合 -->
    <c:forEach var="me" items="${fileNameMap}">
        <c:url value="/DownLoadServlet" var="downurl">
            <c:param name="filename" value="${me.key}"></c:param>
        </c:url>
        ${me.value}<a href="${downurl}">下载</a>
        <br/>
    </c:forEach>
</body>
</html>

③、创建处理下载文件的DownloadServlet,代码如下:

因为要下载的文件可以是各种类型的文件,所以要将文件传送给客户端,其相应的内容应该被当作二进制来处理,所以应该调用response.getOutputStream方法返回ServletOutputStream对象来向客户端写入文件内容。

package com.thr;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;

/**
 * @author tanghaorong
 * @date 2020-05-10
 * @desc 文件下载代码
 */
@WebServlet(name = "DownLoadServlet",value = "/DownLoadServlet")
public class DownLoadServlet extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException,IOException {
        //得到要下载的文件名
        String fileName = request.getParameter("filename");  //23239283-92489-阿凡达.avi
        fileName = new String(fileName.getBytes("iso8859-1"),"UTF-8");
        //上传的文件都是保存在/WEB-INF/upload目录下的子目录当中
        String fileSaveRootPath=this.getServletContext().getRealPath("/WEB-INF/upload");
        //通过文件名找出文件的所在目录
        String path = findFileSavePathByFileName(fileName,fileSaveRootPath);
        //得到要下载的文件
        File file = new File(path + "\\" + fileName);
        //如果文件不存在
        if(!file.exists()){
            request.setAttribute("message", "您要下载的资源已被删除!!");
            request.getRequestDispatcher("/message.jsp").forward(request, response);
            return;
        }
        //处理文件名
        String realname = fileName.substring(fileName.indexOf("_")+1);
        //设置响应头,控制浏览器下载该文件
        response.setHeader("content-disposition", "attachment;filename=" + URLEncoder.encode(realname, "UTF-8"));
        //读取要下载的文件,保存到文件输入流
        FileInputStream in = new FileInputStream(path + "\\" + fileName);
        //创建输出流
        OutputStream out = response.getOutputStream();
        //创建缓冲区
        byte buffer[] = new byte[1024];
        int len = 0;
        //循环将输入流中的内容读取到缓冲区当中
        while((len=in.read(buffer))>0){
            //输出缓冲区的内容到浏览器,实现文件下载
            out.write(buffer, 0, len);
        }
        //关闭文件输入流
        in.close();
        //关闭输出流
        out.close();
    }

    /**
     * @Method: findFileSavePathByFileName
     * @Description: 通过文件名和存储上传文件根目录找出要下载的文件的所在路径
     * @param filename 要下载的文件名
     * @param saveRootPath 上传文件保存的根目录,也就是/WEB-INF/upload目录
     * @return 要下载的文件的存储目录
     */
    public String findFileSavePathByFileName(String filename,String saveRootPath){
        int hashcode = filename.hashCode();
        int dir1 = hashcode&0xf;  //0--15
        int dir2 = (hashcode&0xf0)>>4;  //0-15
        String dir = saveRootPath + "\\" + dir1 + "\\" + dir2;  //upload\2\3  upload\3\5
        File file = new File(dir);
        if(!file.exists()){
            //创建目录
            file.mkdirs();
        }
        return dir;
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
}

④、运行结果:

访问ListFileServlet,就可以在download.jsp页面中显示提供给用户下载的文件资源,如下图所示:

image

然后点击【下载】超链接,将请求提交到DownLoadServlet就行处理就可以实现文件下载了。

参考资料:

9、jQuery中的Ajax的使用

9.1 Ajax的简介

AJAX全称为 "Asynchronous JavaScript And XML" (异步JavaScript和XML),它并不是一种新技术,是由JavaScript、XML、XMLHttpRequest组合在一起、能实现异步提交的功能,是一种创建交互式网页应用的网页开发技术。作用是通过在后台与服务器进行少量数据交换,使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新

『同步』『异步』是一对相对的概念,那么什么是同步,什么是异步呢?

[1] 同步:多个操作按顺序执行,前面的操作没有完成,后面的操作就必须等待。所以同步操作通常是串行的。

image

[2]异步:多个操作相继开始并发执行,即使开始的先后顺序不同,但是由于它们各自是在自己独立的进程或线程中完成,所以互不干扰谁也不用等谁

image

AJAX 的优势

  • 不需要插件支持
  • 优秀的用户体验
  • 提高 Web 程序的性能
  • 减轻服务器和带宽的负担

AJAX的不足

  • 浏览器对 XMLHttpRequest 对象的支持度不足(IE在5.0及以后版本才支持)
  • 破坏浏览器的“前进”,“后退”按钮的正常功能
  • 对搜索引擎的支持不足

9.2 jQuery中的Ajax

jQuery中对JavaScript原生的Ajax操作进行了封装,大家可以在jQuery中非常方便的使用Ajax。在 jQuery 中$.ajax()方法属于最底层的方法,所以它是最常的一个,额外还提供了load()/$.get()\$.post()方法,$.getScript()$.getJSON()方法。

$.ajax():它是jQuery封装的Ajax最底层的方法,功能是最丰富的的,所以平时都选择使用它。
load()/$.get()和$.post():$.get()与$.post()这两个方法都是以简单的GET或POST请求,用于取代复杂的$.ajax()。
$.getScript()和$.getJSON():$.getJSON方法以GET请求向服务器发起请求,返回的是JSON数据。

以下是 $.ajax() 方法中常用的参数,如下所示:

参数 说明
url 表示请求的地址(默认为当前页面)
type 表示请求的类型是GET或POST请求,默认是GET,要求是String类型的参数。
async 表示请求是同步还是异步的,默认为 true,异步;设置为 false 表示 同步,注意:同步时浏览器会被锁住
data 表示发送给服务器的数据,格式有两种:① name1=value1&name2=value2 ;②
contentType 表示发送给服务器的数据类型,内容编码类型默认为"application/x-www-form-urlencoded",该默认值适合大多数应用场合。而JSON格式为"application/json"
dataType 表示服务器响应的数据类型,常用的数据类型有:text表示返回纯文本字符串,xml 表示返回XML文档,json 表示返回JSON数据等等
success 表示请求成功,响应的回调函数
error 表示请求失败时被调用的函数

简单举例(本例子主要是用来参考Ajax声明格式,例子本身没什么实际意义😂😂):

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Ajax学习Demo</title>
</head>
    <script type="text/javascript">
    // 1.测试JSON字符串和JSON对象的互转
    // ①创建一个JSON对象
    var jsonObj = {"stuName":"tom", "stuAge":20};
    console.log(typeof jsonObj);
    console.log(jsonObj);
    console.log(jsonObj.stuName);

    // ②创建一个JSON字符串
    var jsonStr = "{\"stuName\":\"tom\", \"stuAge\":20}";
    console.log(typeof jsonStr);
    console.log(jsonStr);
    console.log(jsonStr.stuName);

    // ③将JSON对象转换为JSON字符串
    jsonStr = JSON.stringify(jsonObj);
    console.log(typeof jsonStr);
    console.log(jsonStr);
    console.log(jsonStr.stuName);

    // ④将JSON字符串解析为JSON对象
    jsonObj = JSON.parse(jsonStr);
    console.log(typeof jsonObj);
    console.log(jsonObj);
    console.log(jsonObj.stuName);

</script>
<!-- 导入jQuery库 -->
<script type="text/javascript" src="${pageContext.request.contextPath}/scripts/jquery-1.7.2.js"></script>
<!-- 开发Ajax程序 -->
<script type="text/javascript">

    $(function () {
        // 1.实验1
        // 浏览器端:发送Ajax请求,携带两个简单的请求参数
        // 服务器端:接收Ajax请求,返回简单的字符串作为响应
        $("#btn01").click(function () {
            $.ajax({
                "url":"${pageContext.request.contextPath}/AjaxServlet",     // Ajax请求要访问的地址
                "type":"post",                                              // 设置请求方式
                "data":{"userName":"tom","password":"123456"},              // Ajax请求要发送的请求参数
                "dataType":"text",                                          // 将服务器端的数据当作普通文本来解析
                "success":function (response) { // 服务器端处理请求成功后调用这个回调函数,response参数是响应体
                    console.log(response);
                },
                "error":function (response) { // 服务器端处理请求失败后调用这个回调函数,response参数是响应体
                    console.log(response);
                    // 响应状态码
                    console.log(response.status);
                    // 响应状态说明
                    console.log(response.statusText);
                    // 响应体
                    console.log(response.responseText);
                }
            });
        });

        // 实验2:
        // 服务器端:返回JSON数据
        $("#btn02").click(function () {
            $.ajax({
                "url":"${pageContext.request.contextPath}/AjaxServlet",
                "type":"post",
                "data":{},
                "dataType":"json",                // 把服务器端返回的数据当作JSON格式解析
                "success":function (response) {   // response是已经解析好的JSON对象
                    console.log(response.empId);
                    console.log(response.empName);
                    console.log(response.empSalary);
                },
                "error":function (response) {
                    console.log(response);
                }
            });

        });

        // 实验3:浏览器端发送JSON请求体
        $("#btn03").click(function () {
            // 1.创建JSON对象,封装一个复杂的JSON
            var student = {
                "stuId":556,
                "stuName":"Thomas",
                "school":{
                    "schoolId":339,
                    "schoolName":"Peking University"
                },
                "subjectList":[
                    {
                        "subjectName":"java",
                        "subjectScore":50
                    },
                    {
                        "subjectName":"PHP",
                        "subjectScore":35
                    },
                    {
                        "subjectName":"python",
                        "subjectScore":24
                    }
                ],
                "teacherMap":{
                    "aaa":{
                        "teacherName":"zhangsan",
                        "teacherAge":20
                    },
                    "bbb":{
                        "teacherName":"zhangsanfeng",
                        "teacherAge":108
                    },
                    "ccc":{
                        "teacherName":"zhangwuji",
                        "teacherAge":25
                    }
                }
            };

            // 2.将JSON对象转换为JSON字符串★
            var jsonStr = JSON.stringify(student);

            // 3.发送Ajax请求
            $.ajax({
                "url":"${pageContext.request.contextPath}/AjaxServlet?method=exer03",
                "type":"post",
                "data":jsonStr,                     // JSON字符串作为请求体数据★
                "contentType":"application/json",   // 告诉服务器端请求体的内容类型是JSON格式★
                "dataType":"text",
                "success":function (response) {
                    console.log(response);
                },
                "error":function (response) {
                    console.log(response);
                }
            });
        });
    });
</script>

<body>
    <button id="btn01">实验1</button>
    <button id="btn02">实验2</button>
    <button id="btn03">实验3</button>
</body>
</html>

9.3 常见响应状态码介绍

  • 100:客户必须继续发出请求
  • 101:客户要求服务器根据请求转换HTTP协议版本
  • 200:请求成功
  • 201:提示知道新文件的URL
  • 202:接受和处理、但处理未完成
  • 203:返回信息不确定或不完整
  • 204:请求收到,但返回信息为空
  • 205:服务器完成了请求,用户代理必须复位当前已经浏览过的文件
  • 206:服务器已经完成了部分用户的GET请求
  • 300:请求的资源可在多处得到
  • 301:删除请求数据
  • 302:在其他地址发现了请求数据
  • 303:建议客户访问其他URL或访问方式
  • 304:客户端已经执行了GET,但文件未变化
  • 305:请求的资源必须从服务器指定的地址得到
  • 306:前一版本HTTP中使用的代码,现行版本中不再使用
  • 307:申明请求的资源临时性删除
  • 400:错误请求,如语法错误
  • 401:请求授权失败
  • 402:保留有效ChargeTo头响应
  • 403:请求不允许
  • 404:没有发现文件、查询或URl
  • 405:用户在Request-Line字段定义的方法不允许
  • 406:根据用户发送的Accept拖,请求资源不可访问
  • 407:类似401,用户必须首先在代理服务器上得到授权
  • 408:客户端没有在用户指定的时间内完成请求
  • 409:对当前资源状态,请求不能完成
  • 410:服务器上不再有此资源且无进一步的参考地址
  • 411:服务器拒绝用户定义的Content-Length属性请求
  • 412:一个或多个请求头字段在当前请求中错误
  • 413:请求的资源大于服务器允许的大小
  • 414:请求的资源URL长于服务器允许的长度
  • 415:请求资源不支持请求项目格式
  • 416:请求中包含Range请求头字段,在当前请求资源范围内没有range指示值,请求也不包含If-Range请求头字段
  • 417:服务器不满足请求Expect头字段指定的期望值,如果是代理服务器,可能是下一级服务器不能满足请求
  • 500:服务器产生内部错误

10、MVC模式设计思想

10.1 MVC模式概念

MVC模式的全名是Model View Controller,是模型(Model )-视图(View )-控制器(Controller)的缩写。

首先要明确的一点是:MVC模式它不是类,也不是什么框架,它只是一种开发的设计思想。将业务逻辑、数据处理、界面显示分别抽取出来统一放到一个地方。从而使同一个程序可以使用不同的表现形式。

MVC的思想就是把我们的程序分为三个核心的模块,这三个模块的详细介绍如下:

  • 模型(Model):负责封装与应用程序的业务逻辑相关的数据以及对数据的处理方法。模型层有对数据直接访问的权力,例如对数据库的访问。它不关心它会如何被视图层显示或被控制器调用,它只接受数据并处理,然后返回一个结果。
  • 视图(View):负责应用程序对用户的显示,它从用户那里获取输入数据并通过控制层传给业务层处理,然后再通过控制层获取业务层返回的结果并显示给用户。
  • 控制器(Controller):负责控制应用程序的流程,它接收从视图层传过来的数据,然后选择Model层中的某个业务来处理,接收Model层返回的结果并选择视图层中的某个视图来显示结果。

我们可以用下图来表示MVC模式中三者之间的关系:

image

MVC模式的开发没有统一的标准,其中最典型的MVC模式开发思想为:JavaBean+JSP+Servlet

  • JavaBean作为模型,既可以作为数据模型来封装业务数据,又可以作为业务逻辑模型来包含应用的业务操作。其中,数据模型用来存储或传递业务数据,而业务逻辑模型接收到控制器传过来的模型更新请求后,执行特定的业务逻辑处理,然后返回相应的执行结果。
  • JSP作为视图,负责提供页面为用户展示数据,提供相应的表单(Form)来用于用户的请求,并在适当的时候(点击按钮)向控制器发出请求来请求模型进行更新。
  • Serlvet作为控制器,用来接收用户提交的请求,然后获取请求中的数据,将之转换为业务模型需要的数据模型,然后调用业务模型相应的业务方法进行更新,同时根据业务执行结果来选择要返回的视图。

不过我们在实际的开发中会把它们拆分的更细,从而形成entity+dto+dao+service+controller+html结构

DTO是Data Transfer Object 的简写,既数据传输对象

 在一个Web服务的实现中,我们常常需要访问数据库,并将从数据库中所取得的数据显示在用户页面中。这样做的一个问题是:用于在用户页面上展示的数据和从数据库中取得的数据常常具有较大区别。在这种情况下,我们常常需要向服务端发送多个请求才能将用于在页面中展示的数据凑齐。

  一个解决该问题的方法就是根据不同需求使用不同的数据表现形式。在一个服务实现中较为常见的数据表现形式有MO(Model Object,在有些上下文中也被称为VO,Value Object)和DTO(Data Transfer Object)。MO用来表示从数据库中读取的数据,而DTO则用来表示在网络上所传输的数据。

详细可见:DTO – 服务实现中的核心数据 - loveis715 - 博客园 (cnblogs.com)

其中:

  • entity+dto+dao+service为模型层
  • controller为控制器层
  • html为视图层。

下面简单介绍一下:

类型 名称 作用
entity 实体类 一般与数据库的表相对应,封装dao层取出来的数据为一个对象,也就是我们常说的pojo,一般只在dao层与service层之间传输。
dao 数据访问层 作用是与数据打交道,可以是数据库操作,也可以是文件读写操作,甚至是redis缓存操作,总之与数据操作有关的都放在这里。
dto 数据传输层 主要用于远程调用等需要大量传输对象的地方。假如一个表有25个数据,而显示的只要5个,所有没必要将所有的属性都传输过去。
service 业务逻辑层 业务逻辑层用于调用dao层,从而处理一下业务逻辑,如拼接SQL,处理事务等。
controller 控制器层 接收从视图层传过来的数据,然后选择service层中的某个业务来处理,接收service层返回的结果并选择视图层中的某个视图来显示结果。

10.2 MVC模式的优缺点

MVC模式之所以能够被广泛的使用到系统中,肯定是有它的优势所在,否则的话也不会存活至今。

MVC的优点:

  1. 降低耦合度:由于模型、视图和控制器各个层都是分离的,这样在某个层需要改变的时候只需要改变相应层中代码即可,而不用关心其它的层。
  2. 重用性高:我们可以在不同的视图来访问同一个服务器端的代码,例如在主界面我可以查询,而在后台也可以使用相同的查询功能,但返回的数据是一样的。
  3. 可维护性高:在修改模型的情况下不会影响到视图,反过来,修改视图,也不会影响到模型。

MVC的缺点:

  1. 完全理解MVC较复杂:实际的项目拆分了很多层,刚开始学习时不知道各个层的作用。记得自己刚刚学习的时候理解了好久。
  2. 代码量增加:既然将项目拆分成很多模块,就必然会导致代码量的增加、相应地也会增加软件开发的成本,设计的难度也会增加。
  3. 系统结构复杂:如果系统小一点到没什么,如果是大型系统的话,使模型、视图与控制器分离,会增加结构的复杂性,并可能产生过多的更新操作,降低运行效率。

总之MVC模式开发的优点是大于缺点的。

10.3 MVC模式的实现

MVC模式的实现:javabean+dao+servlet+jsp实现一个简单的登录功能。

注意:在搞代码是自己遇到了一个问题就是,我明明导入了MySQL的包,但是它还是一直报错com.mysql.jdbc.Driver找不到,然后百度了好久,说是将MySQL的驱动包复制到Tomcat的lib目录下就可以的,起初我还不信,结果试了一下居然成功了,也不再是为什么。还是自己太菜了image

①、创建一个名为user的数据库,然后创建一个名为t_user的表,如下:

image

②、创建视图层页面,login.jsp代码:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head>
        <title>登录页面</title>
    </head>

    <body>
        <h1 style="color: red;">欢迎您登录系统</h1><hr/>
        <div>
            <form method="post" action="/login">
                <table>
                    <tr>
                        <td>Username:</td>
                        <td><input type="text" name="username" value="${username}"/></td>
                    </tr>

                    <tr>
                        <td>Password:</td>
                        <td><input type="password" name="password" value="${password}"/></td>
                    </tr>

                    <tr>
                        <td></td>
                        <td>
                            <input type="submit" value="登录"/>
                            <input type="reset" value="重置"/>
                            <span style="color: red">${error}</span>
                        </td>
                    </tr>
                </table>
            </form>
        </div>
    </body>
</html>

success.jsp代码:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head>
        <title>登录成功</title>
    </head>

    <body>
        <div>
            <h2>欢迎登陆</h2>
            当前登陆的用户为:${currentUser.userName}
        </div>
    </body>
</html>

③、创建模型层,User实体代码:

package com.thr.entity;

/**
 * @author tanghaorong
 * @date 2020-05-13
 * @desc 模型层
 */
public class User {
    private Integer id;
    private String userName;
    private String password;

    //get、set、toSting方法省略
}

对象数据操作的UserDao层代码:

package com.thr.dao;
import com.thr.entity.User;
import java.sql.*;

/**
 * @author tanghaorong
 * @date 2020-05-13
 * @desc 模型层
 */
public class UserDao {

    public User login(String username,String password){
        //创建JDBC的一些对象
        Connection con = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        User resultUser = null;

        try {
            //加载驱动
            Class.forName("com.mysql.jdbc.Driver");
            String url = "jdbc:mysql://localhost:3306/user?characterEncoding=UTF-8";
            String name = "root";
            String pwd = "root";
            //创建连接
            con = DriverManager.getConnection(url, name, pwd);
            String sql = "select * from t_user where username = ? and password = ?";
            //预编译SQL
            ps = con.prepareStatement(sql);
            ps.setString(1,username);
            ps.setString(2,password);
            //执行SQL,并返回结果
            rs = ps.executeQuery();
            while (rs.next()){
                resultUser= new User();
                //将结果封装到User对象中
                resultUser.setId(rs.getInt(1));
                resultUser.setUserName(rs.getString(2));
                resultUser.setPassword(rs.getString(3));
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            //关闭连接
            if(rs!=null){
                try {
                    rs.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(ps!=null){
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(con!=null){
                try {
                    con.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
        //返回User对象
        return resultUser;
    }
}

④、控制器层,UserServlet代码:

package com.thr.controller;

import com.thr.dao.UserDao;
import com.thr.entity.User;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
 * @author tanghaorong
 * @date 2020-05-13
 * @desc 控制器层
 */
@WebServlet(name = "UserServlet",value = "/login")
public class UserServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        //获取前端表单发来的数据
        String userName=request.getParameter("username");
        String password=request.getParameter("password");
        //调用Dao模型层,从数据库中去数据
        UserDao userDao = new UserDao();
        User currentUser=userDao.login(userName, password);
        if(currentUser==null){
            request.setAttribute("error", "用户名或密码错误");
            request.setAttribute("userName", userName);
            request.setAttribute("password", password);
            request.getRequestDispatcher("login.jsp").forward(request, response);
        }else{
            HttpSession session=request.getSession();
            session.setAttribute("currentUser", currentUser);
            response.sendRedirect("success.jsp");
        }
    }
}

⑤、运行结果,如下所示:

image

posted @ 2022-09-04 14:43  Angelzheng  阅读(94)  评论(0编辑  收藏  举报