JavaWeb2

动态资源分为Servlet和JSP,前者是基础。

简介

Sun公司在其API中提供了一个Servlet接口,用户若想开发一个动态web资源(即开发一个java程序向浏览器输出数据),需要完成以下两个步骤:

  • 编写一个Java类,实现Servlet接口;
  • 把开发好的Java类部署到web服务器;

生命周期

如同人一样,六岁上学,十八岁上大学,三十岁结婚,六十岁退休……这些事件是和人这个对象的生命周期相关的,到了某个特定时间就会做。同样,Servlet中有些方法和Servlet的生命周期也是相关的,文档中原文

1
2
3
4
5
This interface defines methods to initialize a servlet, to service requests, and to remove a servlet from the server. These are known as life-cycle methods and are called in the following sequence: 

1. The servlet is constructed, then initialized with the init method.
2. Any calls from clients to the service method are handled.
3. The servlet is taken out of service, then destroyed with the destroy method, then garbage collected and finalized.

也就是初始化init()、响应请求service()、移除destory()这些方法在某个特定时刻必定会执行。具体来说,客户端第一次访问web资源时会创建一个servlet对象,随后init方法会完成对象的初始化,servlet对象不会随着此次访问结束而摧毁,而是留于内存中,客户端访问调用service方法,调用service方法时先创建request对象和response对象,直至关闭服务器或者web应用从服务器删除才会调用servlet的destory()方法。

初试

  1. 在tomcat中新建一个servlettest应用,在应用中新建一个WEB-INF/classes目录;
  2. 在classes目录中新建一个FirstServlet.java
1
2
3
4
5
6
7
8
9
10
11
package cn.itcast;
import java.io.*;
import javax.servlet.*;

public class FirstServlet extends GenericServlet {
public void service(ServletRequest req,ServletResponse res) throws ServletException,java.io.IOException
{
OutputStream out = res.getOutputStream();
out.write("hello world".getBytes());
}
}
  1. 进入classes文件夹,进行编译,但需先导入jar包
1
set classpath=%classpath%;E:\apache-tomcat-8.5.45\lib\servlet-api.jar

再进行编译

1
javac -d . FirstServlet.java
  1. 编写配置文件,在WEB-INF目录下新建web.xml,配置servlet对外访问路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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_3_1.xsd"
version="3.1">

<servlet>
<servlet-name>FirstServlet</servlet-name>
<servlet-class>cn.itcast.FirstServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>FirstServlet</servlet-name>
<url-pattern>/FirstServlet</url-pattern>
</servlet-mapping>

</web-app>
  1. 启动服务器,浏览器地址栏输入http://localhost:8080/servlettest/FirstServlet即可。

在Eclipse中初始

参考这几个博客:

  1. https://blog.csdn.net/ldw4033/article/details/18313281

这篇介绍的最基本的,截至2.3都是没问题的,但是按照他的方法启动有些问题,浏览器一直404,也可能是自己启动方法不对;

  1. https://blog.csdn.net/u014079773/article/details/51397850

这篇主要参考设置服务器,也就是第10项;

  1. https://www.cnblogs.com/SamWeb/p/7627044.html

这篇主要参考最后总结修改class文件生成路径部分;

  1. 参考以上,最后右击项目,Run As=>Run on Server,成功。

  2. 但是有一个问题,在工程的web.xml下也没有对servlet进行配置,如何成功的呢?这里存疑。

    (找到了!!!在servlet3.0后,eclipse中开发服务器的时候,Eclipse不会自动在web.xml中生成该Servlet对应的mapping信息,而是在Servlet代码中加入注解@WebServlet。)

Servlet接口实现类

Servlet接口Sun公司定义了两个默认实现类,分别为:GenericServlet和HttpServlet,前者就是之前讲过的,后者指能够处理HTTP请求的servlet,它在原有接口上添加了一些与HTTP协议处理方法,它比servlet接口的功能更为强大,因此在开发过程中,通常应该继承这个类,在刚才Eclipse自动创建servlet对象中也是继承的这个类。

HttpServlet在实现Servlet接口时,覆写了service方法,该方法体内的代码会自动判断用户的请求方式,如为GET请求,则调用doGet方法;如为POST请求,则调用doPost方法,因此,在实际开发中,通常只需要覆写这两种方法,而不去覆写service方法。

Servlet的一些细节

web.xml配置

这部分内容其实已经不需要了,但是为了深入了解,还是在此记录,简单学习一下。

由于客户端是通过URL地址访问web服务器中的资源,所以Servlet程序若想被外界访问,必须把servlet程序映射到一个URL地址上,这个工作在web.xml文件中使用< servlet >元素和< servlet-mapping >元素完成。

< servlet >元素用于注册Servlet,它包含两个主要的元素:< servlet-name >和< servlet-class >,分别用于设置Servlet的注册名称和Servlet的完整类名。

< servlet-mapping >元素用于映射一个已注册的Servlet的一个对外访问路径,也就是说同一个Servlet可以被映射到多个URL上,每一个< servlet-mapping >包含两个子元素:< servlet-name >和< url-pattern >,分别用于指定Servlet的注册名称和Servlet的对外访问路径。

如果在< servlet >元素中配置了一个< load-on-startup >元素,那么web应用程序在启动时,就会装载并创建Servlet的实例对象以及调用Servlet实例对象的init()方法,< load-on-startup >2< /load-on-startup >中的数字代表创建的优先级,正整数越小代表优先级越高。

Servlet引擎

需要说明的是,Servlet是一个供其他Java程序调用的Java类,它不能独立运行,它的运行完全由Servlet引擎来控制和调度。这里的Servlet引擎其实就是调用它的其他Java程序,也就是web服务器。

缺省Servlet

如果某个Servlet的映射路径仅仅为一个正斜杠/,那么这个Servlet就成为当前web应用的缺省Servlet。

凡是在web.xml文件中找不到匹配的< servlet-mapping >元素的URL,它们的访问请求都将交给缺省Servlet处理,也就是说,缺省Servlet用于处理其他Servlet都不处理的访问请求。

当访问Tomcat服务器中某个静态HTML文件或者图片时,实际上是在访问这个缺省Servlet。也就是说你在浏览器中输入动态web资源也好,静态web资源也好,其实浏览器客户端都交给了Servlet,只不过输入静态web资源,例如访问xxxxxxxx/a.html,这个a.html资源并没有在< servlet-mapping >中进行映射,所以调用缺省Servlet,缺省Servlet寻找同名的a.html进行显示,若缺省Servlet也找不到,则返回404界面。

线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Webtest extends HttpServlet {
int i = 0;
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
i++;
try{
Thread.sleep(1000*10);
}catch(InterruptedException e){
e.printStackTrace();
}
response.getOutputStream().write((i+"").getBytes());
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}

假设有两个用户,第一个用户的页面应该输出1,但是假如在该用户线程休眠过程中有另一个用户访问,则最后输出2,这样就涉及到了线程安全,当然,可以通过synchronized()方法来保证线程安全

1
2
3
4
5
6
7
8
9
synchronized(this){
i++;
try{
Thread.sleep(1000*10);
}catch(InterruptedException e){
e.printStackTrace();
}
response.getOutputStream().write((i+"").getBytes());
}

但是,很显然在实际开发中,不可能这么做,否则一个用户访问其他用户只能等待。还有另外一种方法

1
public class Webtest extends HttpServlet implements SingleThreadModel

SingleThreadModel接口中没有定义任何内容,称为标记接口,相当于只是给该类贴了一个线程安全的标签,这种方法在servlet 2.4之后就已经Deprecated。

线程安全问题很大部分是由实例变量造成的,只要在Servlet里面的任何方法里面都不使用实例变量,那么该Servlet就是线程安全的。所以,在Servlet中避免使用实例变量是保证Servlet线程安全的最佳选择。

Java 内存模型中,方法中的临时变量是在栈上分配空间,而且每个线程都有自己私有的栈空间,所以它们不会影响线程的安全。

Servlet对象

服务器在创建Servlet时,除了有request、response对象,还有ServletConfig、ServletContext、Session、Cookie等对象。

ServletConfig对象

在Servlet配置文件中,可以在< servlet >元素中使用一个或多个< init-param >标签为Servlet配置一些初始化参数,配置之后,web容器在创建Servlet实例对象时,会自动将这些初始化参数封装到ServletConfig对象中,并在调用init()方法时,将ServletConfig对象传递给servlet,进而,开发人员通过ServletConfig对象就可以得到当前servlet的初始化参数信息。

1
2
3
4
<init-param>
<param-name>date</param-name>
<param-value>2019</param-value>
</init-param>

java程序中调用即可得到“2019”:

1
String value = this.getServletConfig().getInitParameter("data");

在实际开发中,例如字符码表、要连接的数据库以及读取哪个配置文件等不适合在程序中显示,就用到了ServletConfig。

1
2
3
4
<init-param>
<param-name>charset</param-name>
<param-value>UTF-8</param-value>
</init-param>
1
2
3
4
5
6
7
8
9
10
11
12
<init-param>
<param-name>url</param-name>
<param-value>jdbc:mysql://localhost:3306/test</param-value>
</init-param>
<init-param>
<param-name>username</param-name>
<param-value>root</param-value>
</init-param>
<init-param>
<param-name>password</param-name>
<param-value>root</param-value>
</init-param>
1
2
3
4
<init-param>
<param-name>config</param-name>
<param-value>/struts-config.xml</param-value>
</init-param>

ServletContext对象

web容器在启动时,会为每个web应用都创建一个ServletContext对象,ServletConfig对象中维护ServletContext对象的引用,可以通过ServletConfig().ServletContext()方法获得ServletContext对象。、

由于一个web应用中所有Servlet共用一个ServletContext对象,因此Servlet对象之间可以通过ServletContext对象实现通讯。ServletContext对象通常也称之为context域对象。

在实际开发中,ServletContext具体有以下几个应用:

  • 多个servlet之间实现数据共享,一个写入,其他就可以读到;
  • 获取WEB应用的初始化参数,和上边ServletConfig设置初始化参数类似,只不过这里设置的是整个web应用,使用< context-param >
  • 实现servlet转发,关于转发和重定向的区别,打个比方,你找我借钱,我让你去找他借,这是重定向;你找我借钱,我帮你去找他借,这是转发。在开发中,转发用的很多,比如servlet的内容肯定要转发,让html去排版再在浏览器显示。
1
2
RequestDispatcher rd = this.getServletContext().getRequestDispatcher("/1.jsp");
rd.forward(request,response);
  • 读取资源文件

假设读取应用下的db.properties:(常见的两种配置文件,如果配置文件内数据有关联,则用xml,否则properties)

1
2
3
url=jdbc:mysql://localhost:3306/test
username=root
password=root

在servlet中读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
InputStream in = this.getServletContext().getResourceAsStream("/WEB-INF/classes/db.properties");
//不能使用传统方法
//FileInputStream in = new FileInputStream("classes/db.properties");
//因为执行这行代码的是JVM,也就是说相对路径是以Tomcat下startup.bat来看的,此时并没有classes目录
//要想使用,应该采用如下方法;
//String path = this.getServletContext().getRealPath("/WEB-INF/classes/db.properties");
//FileInputStream in = new FileInputStream(path);
Properties props = new properties(); //map
props.load(in); //将in的内容装载到props

String url = props.getProperty("url");
String username = props.getProperty("username");
String password = props.getProperty("password");

System.out.println(url);
System.out.println(username);
System.out.println(password);
-------------------------------------------------------------------
>>jdbc:mysql://localhost:3306/test
>>root
>>root

如果读取资源文件的不是servlet,而是普通的Java文件,就只能通过类加载器,例如在dao读取db.properties:

1
2
3
4
5
#servlet
……………………
UserDao dao = new UserDao();
dao.update();
……………………
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#UserDao.java
package cn.itcast.dao;
import java.io.InputStream;
import java.io.IOException;
import java.util.Properties;

public class UserDao(
public void update() throws IOException {
InputStream in = UserDao.class.getClassLoader().getResourceAsStream("db.properties");
Properties props = new properties();
props.load(in);

String url = props.getProperty("url");
String username = props.getProperty("username");
String password = props.getProperty("password");

System.out.println(url);
System.out.println(username);
System.out.println(password);
}
)

扩展一:

类加载器读取的文件不能太大,因为类加载器加载的文件直接存在内存中,文件太大会导致内存溢出,JVM爆掉。

扩展二:

UserDao.java中除了update方法还可能有find、delete等方法,但是这写方法只加载一次就可以,不用每个方法里都加载,所以可以写成静态代码块,实际开发中常这样写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class UserDao(
private static Properties dbconfig = new Properties();
static {
try{
InputStream in = UserDao.class.getClassLoader().getResourceAsStream("db.properties");
dbconfig.load(in);
}catch (Exception e){
throw new ExceptionInInitializerError(e);
}
}
public void update() throws IOException {
System.out.println(dbconfig.getProperty("url"));
}
public void find() {

}
)

扩展三:

这样的话,启动服务器后,在db.properties中进行修改,这边是无法更新的,因为类加载加载到内存中,要想更新,只能重启服务器,显然不是想要的,所以应该如下

1
2
3
4
5
6
7
8
9
10
11
12
13
public class UserDao(
public void update() throws IOException {
//通过类装载的方式得到资源文件的位置,再通过传统方式读取资源文件的数据,这样可以读取到更新后的数据
String path = UserDao.class.getClassLoader().getResource("db.properties").getPath();
FileInputStream in = new FileInputStream(path);
Properties dbconfig = new Properties();
dbconfig.load(in);
System.out.println(dbconfig.getProperty("url"));
}
public void find() {

}
)

:转载文章请注明出处,谢谢~