0%

过滤器

当浏览器发送请求给服务器的时候,先执行过滤器,然后才访问Web的资源。服务器响应Response,从Web资源抵达浏览器之前,也会途径过滤器。

过滤一些敏感的字符串【规定不能出现敏感字符串】、避免中文乱码【规定Web资源都使用UTF-8编码】、权限验证【规定只有带Session或Cookie的浏览器,才能访问web资源】等等等

也就是说:当需要限制用户访问某些资源时、在处理请求时提前处理某些资源、服务器响应的内容对其进行处理再返回、我们就是用过滤器来完成的!

filter是如何实习拦截的?

Filter接口中有一个doFilter方法,当我们编写好Filter,并配置对哪个web资源进行拦截后,WEB服务器每次在调用web资源的service方法之前,都会先调用一下filter的doFilter方法,因此,在该方法内编写代码可达到如下目的:

  1. 调用目标资源之前,让一段代码执行。
  2. 是否调用目标资源(即是否让用户访问web资源)。
  3. 调用目标资源之后,让一段代码执行。

  web服务器在调用doFilter方法时,会传递一个filterChain对象进来,filterChain对象是filter接口中最重要的一个对 象,它也提供了一个doFilter方法,开发人员可以根据需求决定是否调用此方法,调用该方法,则web服务器就会调用web资源的service方 法,即web资源就会被访问,否则web资源不会被访问。

api

1
2
3
4
5
public interface Filter{
void init(FilterConfig varl);
void doFilter(ServletRequest val1,ServletResponse val2,FilterChain val3);
void destroy();
}

创建:

filter的创建和销毁是由web服务器负责的。web程序启动时,web服务器将创建Filter 的实例对象,并调用其init方法,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作,filter对象只会创建一次,init方法也只会执行一次。通过init方法的参数,可获得代表当前filter配置信息的FilterConfig对象。

销毁:

Web容器调用destroy方法销毁Filter。destroy方法在Filter的生命周期中仅执行一次。在destroy方法中,可以释放过滤器使用的资源。

filter开发和部署

Filter开发分为二个步骤:

  1. 编写java类实现Filter接口,并实现其doFilter方法。
  2. 在 web.xml 文件中使用<filter>和<filter-mapping>元素对编写的filter类进行注册,并设置它所能拦截的资源。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* @Description:filter的三种典型应用:
* 1、可以在filter中根据条件决定是否调用chain.doFilter(request, response)方法,
* 即是否让目标资源执行
* 2、在让目标资源执行之前,可以对request\response作预处理,再让目标资源执行
* 3、在目标资源执行之后,可以捕获目标资源的执行结果,从而实现一些特殊的功能
*/
public class FilterDemo01 implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("----过滤器初始化----");
}

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

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

System.out.println("FilterDemo01执行前!!!");
chain.doFilter(request, response); //让目标资源执行,放行
System.out.println("FilterDemo01执行后!!!");
}

@Override
public void destroy() {
System.out.println("----过滤器销毁----");
}
}

filter的部署

注册filter

<filter>用于注册过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   <filter>
<description>FilterDemo02过滤器</description>
<filter-name>FilterDemo02</filter-name>
<filter-class>me.gacl.web.filter.FilterDemo02</filter-class>
<!--配置FilterDemo02过滤器的初始化参数-->
<init-param>
<description>配置FilterDemo02过滤器的初始化参数</description>
<param-name>name</param-name>
<param-value>gacl</param-value>
</init-param>
<init-param>
<description>配置FilterDemo02过滤器的初始化参数</description>
<param-name>like</param-name>
<param-value>java</param-value>
</init-param>
</filter>
  • <filter-name>用于为过滤器指定一个名字,该元素的内容不能为空。
  • <filter-class>元素用于指定过滤器的完整的限定类名
  • <init-param>元素用于为过滤器指定初始化参数,它的子元素、<param-name>指定参数的名字,<param-value>指定参数的值。在过滤器中,可以使用FilterConfig接口对象来访问初始化参数
  • <description>用于添加描述信息,该元素的内容可为空,<description>可以不配置。

filter-mapping(映射过滤器)

<filter-mapping>元素用于设置一个Filter 所负责拦截的资源

一个Filter拦截的资源可通过两种方式来指定:Servlet 名称和资源访问的请求路径

1
2
3
4
<filter-mapping>
<filter-name>FilterDemo1</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
  • <filter-name>子元素用于设置filter的注册名称。该值必须是在元素中声明过的过滤器的名字
  • <url-pattern>设置 filter 所拦截的请求路径(过滤器关联的URL样式)
  • <servlet-name>指定过滤器所拦截的Servlet名称
  • <dispatcher>指定过滤器所拦截的资源被 Servlet 容器调用的方式,可以是REQUEST,INCLUDE,FORWARD和ERROR之一,默认REQUEST。用户可以设置多个<dispatcher> 子元素用来指定 Filter 对资源的多种调用方式进行拦截。

dispatcher

子元素可以设置的值及其意义:

  • REQUEST:当用户直接访问页面时,Web容器将会调用过滤器。如果目标资源是通过RequestDispatcher的include()或forward()方法访问时,那么该过滤器就不会被调用。
  • INCLUDE:如果目标资源是通过RequestDispatcher的include()方法访问时,那么该过滤器将被调用。除此之外,该过滤器不会被调用。
  • FORWARD:如果目标资源是通过RequestDispatcher的forward()方法访问时,那么该过滤器将被调用,除此之外,该过滤器不会被调用。
  • ERROR:如果目标资源是通过声明式异常处理机制调用时,那么该过滤器将被调用。除此之外,过滤器不会被调用。

通过注解配置

1
2
    @WebFilter(filterName = "FilterDemo1",urlPatterns = "/*")
//:“/*”的含义是所有的Web资源都需要途径过滤器

Filter链

​ 在一个web应用中,可以开发编写多个Filter,这些Filter组合起来称之为一个Filter链。
  web服务器根据Filter在web.xml文件中的注册顺序,决定先调用哪个Filter,当第一个Filter的doFilter方法被调用时,web服务器会创建一个代表Filter链的FilterChain对象传递给该方法。在doFilter方法中,开发人员如果调用了FilterChain对象的doFilter方法,则web服务器会检查FilterChain对象中是否还有filter,如果有,则调用第2个filter,如果没有,则调用目标资源。

简单应用

javaweb学习总结(四十三)——Filter高级开发 - 孤傲苍狼 - 博客园 (cnblogs.com)
javaweb学习总结(四十六)——Filter(过滤器)常见应用 - 孤傲苍狼 - 博客园 (cnblogs.com)

filter的三种典型应用:

  • 1、可以在filter中根据条件决定是否调用chain.doFilter(request, response)方法,即是否让目标资源执行
  • 2、在让目标资源执行之前,可以对requestresponse作预处理,再让目标资源执行
  • 3、在目标资源执行之后,可以捕获目标资源的执行结果,从而实现一些特殊的功能

禁止浏览器缓存所有动态页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {

//让Web资源不缓存,很简单,设置http中response的请求头即可了!

//我们使用的是http协议,ServletResponse并没有能够设置请求头的方法,所以要强转成HttpServletRequest

//一般我们写Filter都会把他俩强转成Http类型的
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;

response.setDateHeader("Expires", -1);
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "no-cache");

//放行目标资源的response已经设置成不缓存的了
chain.doFilter(request, response);
}
  • 没有过滤之前,响应头是这样的:

  • 过滤之后,响应头是这样的:


实现自动登陆

开发实体、集合模拟数据库、Dao

  • 实体:
1
2
3
4
5
6
7
8
9
10
11
12
13
private String username ;
private String password;


public User() {
}

public User(String username, String password) {
this.username = username;
this.password = password;
}

//各种setter和getter
  • 集合模拟数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class UserDB {

private static List<User> users = new ArrayList<>();



static {
users.add(new User("aaa", "123"));
users.add(new User("bbb", "123"));
users.add(new User("ccc", "123"));
}

public static List<User> getUsers() {
return users;
}

public static void setUsers(List<User> users) {
UserDB.users = users;
}
}
  • 开发dao
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    public User find(String username, String password) {

List<User> userList = UserDB.getUsers();

//遍历List集合,看看有没有对应的username和password
for (User user : userList) {
if (user.getUsername().equals(username) && user.getPassword().equals(password)) {
return user;
}
}
return null;
}
//根据用户名可以找到用户
public User find(String username) {
List<User> userList = UserDB.getUsers();

//遍历List集合,看看有没有对应的username和password
for (User user : userList) {
if (user.getUsername().equals(username)) {
return user;
}
}

return null;
}

登陆界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<form action="${pageContext.request.contextPath}/LoginServlet">

用户名<input type="text" name="username">
<br>
密码<input type="password" name="password">
<br>

<input type="radio" name="time" value="10">10分钟
<input type="radio" name="time" value="30">30分钟
<input type="radio" name="time" value="60">1小时
<br>

<input type="submit" value="登陆">

</form>

处理登陆的Servlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//得到客户端发送过来的数据
String username = request.getParameter("username");
String password = request.getParameter("password");

UserDao userDao = new UserDao();
User user = userDao.find(username, password);

if (user == null) {
request.setAttribute("message", "用户名或密码是错的!");
request.getRequestDispatcher("/message.jsp").forward(request, response);
}

//如果不是为空,那么在session中保存一个属性
request.getSession().setAttribute("user", user);
request.setAttribute("message", "恭喜你,已经登陆了!");

//如果想要用户关闭了浏览器,还能登陆,就必须要用到Cookie技术了
Cookie cookie = new Cookie("autoLogin", user.getUsername() + "." + user.getPassword());

//设置Cookie的最大声明周期为用户指定的
cookie.setMaxAge(Integer.parseInt(request.getParameter("time")) * 60);

//把Cookie返回给浏览器
response.addCookie(cookie);

//跳转到提示页面
request.getRequestDispatcher("/message.jsp").forward(request, response);

过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
        HttpServletResponse response = (HttpServletResponse) resp;
HttpServletRequest request = (HttpServletRequest) req;

//如果用户没有关闭浏览器,就不需要Cookie做拼接登陆了
if (request.getSession().getAttribute("user") != null) {
chain.doFilter(request, response);
return;
}

//用户关闭了浏览器,session的值就获取不到了。所以要通过Cookie来自动登陆
Cookie[] cookies = request.getCookies();
String value = null;
for (int i = 0; cookies != null && i < cookies.length; i++) {
if (cookies[i].getName().equals("autoLogin")) {
value = cookies[i].getValue();
}
}


//得到Cookie的用户名和密码
if (value != null) {

String username = value.split("\\.")[0];
String password = value.split("\\.")[1];

//在Cookie拿到的密码是md5加密过的,不能直接与数据库中的密码比较
UserDao userDao = new UserDao();
User user = userDao.find(username);

//通过用户名获得用户信息,得到用户的密码,用户的密码也md5一把

String dbPassword = md5.md5(user.getPassword());
//如果两个密码匹配了,就是正确的密码了
if (password.equals(dbPassword)) {
request.getSession().setAttribute("user", user);
}

}
;

过滤敏感词

要实现这样的功能也很简单,用户输入的敏感词肯定是在getParameter()获取的,我们在getParameter()得到这些数据的时候,判断有没有敏感词汇,如果有就替换掉就好了!简单来说:也是要增强request对象

增强request对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class MyDirtyRequest extends HttpServletRequestWrapper {

HttpServletRequest request;

//定义一堆敏感词汇
private List<String> list = Arrays.asList("傻b", "尼玛", "操蛋");

public MyDirtyRequest(HttpServletRequest request) {
super(request);
this.request = request;
}

@Override
public String getParameter(String name) {

String value = this.request.getParameter(name);

if (value == null) {
return null;
}

//遍历list集合,看看获取得到的数据有没有敏感词汇
for (String s : list) {

if (s.equals(value)) {
value = "*****";
}
}

return value ;
}
}

开发过滤器

1
2
3
4
5
6
7
8
9
10
11
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {

//将request和response强转成http协议的
HttpServletRequest httpServletRequest = (HttpServletRequest) req;
HttpServletResponse httpServletResponse = (HttpServletResponse) resp;

MyDirtyRequest dirtyRequest = new MyDirtyRequest(httpServletRequest);

//传送给目标资源的是被增强后的request对象
chain.doFilter(dirtyRequest, httpServletResponse);
}

JDBC

——执行SQL语句的java api

操作数据库都是在JDBC API【接口】上,使用不同的数据库,只要用数据库厂商提供的数据库驱动程序即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;

try {

/*
* 加载驱动有两种方式
*
* 1:会导致驱动会注册两次,过度依赖于mysql的api,脱离的mysql的开发包,程序则无法编译
* 2:驱动只会加载一次,不需要依赖具体的驱动,灵活性高
*
* 我们一般都是使用第二种方式
* */

//1.
//DriverManager.registerDriver(new com.mysql.jdbc.Driver());

//2.
Class.forName("com.mysql.jdbc.Driver");

//获取与数据库连接的对象-Connetcion
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/zhongfucheng", "root", "root");

//获取执行sql语句的statement对象
statement = connection.createStatement();

//执行sql语句,拿到结果集
resultSet = statement.executeQuery("SELECT * FROM users");

//遍历结果集,得到数据
while (resultSet.next()) {

System.out.println(resultSet.getString(1));

System.out.println(resultSet.getString(2));
}

} catch (SQLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {

/*
* 关闭资源,后调用的先关闭
*
* 关闭之前,要判断对象是否存在
* */

if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}

}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}

}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}

}

}

DriverManager类

Jdbc程序中的DriverManager用于加载驱动,并创建与数据库的链接,这个API的常用方法:

  1. DriverManager.registerDriver(new Driver())
  2. DriverManager.getConnection(url, user, password),

  注意:在实际开发中并不推荐采用registerDriver方法注册驱动。原因有二:
    1、查看Driver的源代码可以看到,如果采用此种方式,会导致驱动程序注册两次,也就是在内存中会有两个Driver对象。
    2、程序依赖mysql的api,脱离mysql的jar包,程序将无法编译,将来程序切换底层数据库将会非常麻烦。

  推荐方式:Class.forName(“com.mysql.jdbc.Driver”);
  采用此种方式不会导致驱动对象在内存中重复出现,并且采用此种方式,程序仅仅只需要一个字符串,不需要依赖具体的驱动,使程序的灵活性更高。

connection类

客户端与数据库所有的交互都是通过Connection来完成的。

常用的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//创建向数据库发送sql的statement对象。

createcreateStatement()

//创建向数据库发送预编译sql的PrepareSatement对象。

prepareStatement(sql)

//创建执行存储过程的callableStatement对象

prepareCall(sql)

//设置事务自动提交

setAutoCommit(boolean autoCommit)

//提交事务

commit()

//回滚事务

rollback()

Statement类

Statement对象用于向数据库发送Sql语句,对数据库的增删改查都可以通过此对象发送sql语句完成。

Statement对象的常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//查询

executeQuery(String sql)

//增删改

executeUpdate(String sql)

//任意sql语句都可以,但是目标不明确,很少用

execute(String sql)

//把多条的sql语句放进同一个批处理中

addBatch(String sql)

//向数据库发送一批sql语句执行

executeBatch()

ResultSet类

ResultSet对象代表Sql语句的执行结果,当Statement对象执行executeQuery()时,会返回一个ResultSet对象

ResultSet对象维护了一个数据行的游标【简单理解成指针】,调用ResultSet.next()方法,可以让游标指向具体的数据行,进行获取该行的数据

常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//获取任意类型的数据

getObject(String columnName)

//获取指定类型的数据【各种类型,查看API】

getString(String columnName)

//对结果集进行滚动查看的方法

next()

Previous()

absolute(int row)

beforeFirst()

afterLast()

PreparedStatement类

PreparedStatement对象继承Statement对象,它比Statement对象更强大,使用起来更简单

  1. Statement对象编译SQL语句时,如果SQL语句有变量,就需要使用分隔符来隔开,如果变量非常多,就会使SQL变得非常复杂。PreparedStatement可以使用占位符,简化sql的编写
  2. Statement会频繁编译SQL。PreparedStatement可对SQL进行预编译,提高效率,预编译的SQL存储在PreparedStatement对象中
  3. PreparedStatement防止SQL注入。【Statement通过分隔符’++’,编写永等式,可以不需要密码就进入数据库】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//模拟查询id为2的信息
String id = "2";

Connection connection = UtilsDemo.getConnection();

String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement preparedStatement = connection.preparedStatement(sql);

//第一个参数表示第几个占位符【也就是?号】,第二个参数表示值是多少
preparedStatement.setString(1,id);

ResultSet resultSet = preparedStatement.executeQuery();

if (resultSet.next()) {
System.out.println(resultSet.getString("name"));
}

//释放资源
UtilsDemo.release(connection, preparedStatement, resultSet);

批处理

当需要向数据库发送一批SQL语句执行时,应避免向数据库一条条发送执行,采用批处理以提升执行效率

批处理有两种方式:

  1. Statement
  2. PreparedStatement

通过executeBath()方法批量处理执行SQL语句,返回一个int[]数组,该数组代表各句SQL的返回值

以下代码是以Statement方式实现批处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
* Statement执行批处理
*
* 优点:
* 可以向数据库发送不同的SQL语句
* 缺点:
* SQL没有预编译
* 仅参数不同的SQL,需要重复写多条SQL
* */
Connection connection = UtilsDemo.getConnection();

Statement statement = connection.createStatement();
String sql1 = "UPDATE users SET name='zhongfucheng' WHERE id='3'";
String sql2 = "INSERT INTO users (id, name, password, email, birthday)" +
" VALUES('5','nihao','123','ss@qq.com','1995-12-1')";

//将sql添加到批处理
statement.addBatch(sql1);
statement.addBatch(sql2);

//执行批处理
statement.executeBatch();

//清空批处理的sql
statement.clearBatch();

UtilsDemo.release(connection, statement, null);

以下方式以PreparedStatement方式实现批处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
* PreparedStatement批处理
* 优点:
* SQL语句预编译了
* 对于同一种类型的SQL语句,不用编写很多条
* 缺点:
* 不能发送不同类型的SQL语句
*
* */
Connection connection = UtilsDemo.getConnection();

String sql = "INSERT INTO test(id,name) VALUES (?,?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);

for (int i = 1; i <= 205; i++) {
preparedStatement.setInt(1, i);
preparedStatement.setString(2, (i + "zhongfucheng"));

//添加到批处理中
preparedStatement.addBatch();

if (i %2 ==100) {

//执行批处理
preparedStatement.executeBatch();

//清空批处理【如果数据量太大,所有数据存入批处理,内存肯定溢出】
preparedStatement.clearBatch();
}

}
//不是所有的%2==100,剩下的再执行一次批处理
preparedStatement.executeBatch();

//再清空
preparedStatement.clearBatch();

UtilsDemo.release(connection, preparedStatement, null);

Oracle

下面用JDBC连接Oracle数据库去操作大文本数据和二进制数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
//使用JDBC连接Oracle数据库操作二进制数据

/*
* 对于Oracle数据库和Mysql数据库是有所不同的。
* 1.Oracle定义了BLOB字段,但是这个字段不是真正地存储二进制数据
* 2.向这个字段存一个BLOB指针,获取到Oracle的BLOB对象,把二进制数据放到这个指针里面,指针指向BLOB字段
* 3.需要事务支持
*
* */
public class Demo7 {
@Test
public void add() {


Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;

try {
connection = UtilsDemo.getConnection();

//开启事务
connection.setAutoCommit(false);

//插入一个BLOB指针
String sql = "insert into test4(id,image) values(?,empty_blob())";
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setInt(1, 1);
preparedStatement.executeUpdate();

//把BLOB指针查询出来,得到BLOB对象
String sql2 = "select image from test4 where id= ? for update";
preparedStatement = connection.prepareStatement(sql2);
preparedStatement.setInt(1, 1);
resultSet = preparedStatement.executeQuery();

if (resultSet.next()) {
//得到Blob对象--当成是Oracle的Blob,不是JDBC的,所以要强转[导的是oracle.sql.BLOB包]
BLOB blob = (BLOB) resultSet.getBlob("image");

//写入二进制数据
OutputStream outputStream = blob.getBinaryOutputStream();

//获取到读取文件读入流
InputStream inputStream = Demo7.class.getClassLoader().getResourceAsStream("01.jpg");

int len=0;
byte[] bytes = new byte[1024];
while ((len = inputStream.read(bytes)) > 0) {

outputStream.write(bytes, 0, len);
}
outputStream.close();
inputStream.close();
connection.setAutoCommit(true);
}
} catch (SQLException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
UtilsDemo.release(connection, preparedStatement, null);
}

}

@Test
public void find() {

Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;

try {
connection = UtilsDemo.getConnection();
String sql = "SELECT * FROM test4 WHERE id=1";

preparedStatement = connection.prepareStatement(sql);
resultSet = preparedStatement.executeQuery();

if (resultSet.next()) {

//获取到BLOB对象
BLOB blob = (BLOB) resultSet.getBlob("image");

//将数据读取到磁盘上
InputStream inputStream = blob.getBinaryStream();
FileOutputStream fileOutputStream = new FileOutputStream("d:\\zhongfucheng.jpg");
int len=0;
byte[] bytes = new byte[1024];

while ((len = inputStream.read(bytes)) > 0) {

fileOutputStream.write(bytes, 0, len);
}

inputStream.close();
fileOutputStream.close();

}

} catch (SQLException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
UtilsDemo.release(connection, preparedStatement, null);
}
}
}

对于JDBC连接Oracle数据库操作CLOB数据,我就不再重复了,操作跟BLOB几乎相同

获取数据库的自动主键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Test
public void test() {

Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;

try {
connection = JdbcUtils.getConnection();

String sql = "INSERT INTO test(name) VALUES(?)";
preparedStatement = connection.prepareStatement(sql);

preparedStatement.setString(1, "ouzicheng");

if (preparedStatement.executeUpdate() > 0) {

//获取到自动主键列的值
resultSet = preparedStatement.getGeneratedKeys();

if (resultSet.next()) {
int id = resultSet.getInt(1);
System.out.println(id);
}
}

} catch (SQLException e) {
e.printStackTrace();
} finally {
JdbcUtils.release(connection, preparedStatement, null);
}

调用数据库的存储过程

调用存储过程的语法:

1
{call <procedure-name>[(<arg1>,<arg2>, ...)]}

调用函数的语法:

1
{?= call <procedure-name>[(<arg1>,<arg2>, ...)]}

如果是Output类型的,那么在JDBC调用的时候是要注册的。如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*
jdbc调用存储过程

delimiter $$
CREATE PROCEDURE demoSp(IN inputParam VARCHAR(255), INOUT inOutParam varchar(255))
BEGIN
SELECT CONCAT('zyxw---', inputParam) into inOutParam;
END $$
delimiter ;
*/
//我们在JDBC调用存储过程,就像在调用方法一样
public class Demo9 {

public static void main(String[] args) {
Connection connection = null;
CallableStatement callableStatement = null;

try {
connection = JdbcUtils.getConnection();

callableStatement = connection.prepareCall("{call demoSp(?,?)}");

callableStatement.setString(1, "nihaoa");

//注册第2个参数,类型是VARCHAR
callableStatement.registerOutParameter(2, Types.VARCHAR);
callableStatement.execute();

//获取传出参数[获取存储过程里的值]
String result = callableStatement.getString(2);
System.out.println(result);

} catch (Exception e) {
e.printStackTrace();
}finally {
try {
connection.close();
callableStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}

}

参考资料:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
----------------------------------------------------------------------------------过程

#修改mysql语句的结果符为//
mysql > delimiter //

#定义一个过程,获取users表总记录数,将10设置到变量count中
create procedure simpleproc(out count int)
begin
select count(id) into count from users;
end
//

#修改mysql语句的结果符为;
mysql > delimiter ;

#调用过程,将结果覆给变量a,@是定义变量的符号
call simpleproc(@a);

#显示变量a的值
select @a;

//以下是Java调用Mysql的过程
String sql = "{call simpleproc(?)}";
Connection conn = JdbcUtil.getConnection();
CallableStatement cstmt = conn.prepareCall(sql);
cstmt.registerOutParameter(1,Types.INTEGER);
cstmt.execute();
Integer count = cstmt.getInt(1);
System.out.println("共有" + count + "人");

----------------------------------------------------------------------------------函数

#修改mysql语句的结果符为//
mysql > delimiter //

#定义一个函数,完成字符串拼接
create function hello( s char(20) ) returns char(50)
return concat('hello,',s,'!');
//

#修改mysql语句的结果符为;
mysql > delimiter ;

#调用函数
select hello('world');

//以下是Java调用Mysql的函数
String sql = "{? = call hello(?)}";
Connection conn = JdbcUtil.getConnection();
CallableStatement cstmt = conn.prepareCall(sql);
cstmt.registerOutParameter(1,Types.VARCHAR);
cstmt.setString(2,"zhaojun");
cstmt.execute();
String value = cstmt.getString(1);
System.out.println(value);
JdbcUtil.close(cstmt);
JdbcUtil.close(conn);

数据库连接池

数据库连接池负责分配,管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个

编写连接池

  1. 编写连接池需实现java.sql.DataSource接口
  2. 创建批量的Connection用LinkedList保存【既然是个池,当然用集合保存、、LinkedList底层是链表,对增删性能较好】
  3. 实现getConnetion(),让getConnection()每次调用,都是在LinkedList中取一个Connection返回给用户
  4. 调用Connection.close()方法,Connction返回给LinkedList
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private static LinkedList<Connection> list = new LinkedList<>();

//获取连接只需要一次就够了,所以用static代码块
static {
//读取文件配置
InputStream inputStream = Demo1.class.getClassLoader().getResourceAsStream("db.properties");

Properties properties = new Properties();
try {
properties.load(inputStream);
String url = properties.getProperty("url");
String username = properties.getProperty("username");
String driver = properties.getProperty("driver");
String password = properties.getProperty("password");

//加载驱动
Class.forName(driver);

//获取多个连接,保存在LinkedList集合中
for (int i = 0; i < 10; i++) {
Connection connection = DriverManager.getConnection(url, username, password);
list.add(connection);
}

} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}

}

//重写Connection方法,用户获取连接应该从LinkedList中给他
@Override
public Connection getConnection() throws SQLException {
System.out.println(list.size());
System.out.println(list);

//先判断LinkedList是否存在连接
return list.size() > 0 ? list.removeFirst() : null;
}

我们已经完成前三步了,现在问题来了。我们调用Conncetion.close()方法,是把数据库的物理连接关掉,而不是返回给LinkedList的

解决思路:

  1. 写一个Connection子类,覆盖close()方法
  2. 写一个Connection包装类,增强close()方法
  3. 用动态代理,返回一个代理对象出去,拦截close()方法的调用,对close()增强

分析第一个思路:

  • Connection是通过数据库驱动加载的,保存了数据的信息。写一个子类Connection,new出对象,子类的Connction无法直接继承父类的数据信息,也就是说子类的Connection是无法连接数据库的,更别谈覆盖close()方法了。

分析第二个思路:

  • 写一个Connection包装类。
    1. 写一个类,实现与被增强对象的相同接口【Connection接口】
    2. 定义一个变量,指向被增强的对象
    3. 定义构造方法,接收被增强对象
    4. 覆盖想增强的方法
    5. 对于不想增强的方法,直接调用被增强对象的方法
  • 这个思路本身是没什么毛病的,就是实现接口时,方法太多了!,所以我们也不使用此方法

分析第三个思路代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Override
public Connection getConnection() throws SQLException {

if (list.size() > 0) {
final Connection connection = list.removeFirst();

//看看池的大小
System.out.println(list.size());

//返回一个动态代理对象
return (Connection) Proxy.newProxyInstance(Demo1.class.getClassLoader(), connection.getClass().getInterfaces(), new InvocationHandler() {

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

//如果不是调用close方法,就按照正常的来调用
if (!method.getName().equals("close")) {
return method.invoke(connection, args);
} else {

//进到这里来,说明调用的是close方法
list.add(connection);

//再看看池的大小
System.out.println(list.size());

}
return null;
}

});
}
return null;
}

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package me.gacl.demo;

import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.Properties;
import javax.sql.DataSource;
public class JdbcPool implements DataSource{

/**
* @Field: listConnections
* 使用LinkedList集合来存放数据库链接,
* 由于要频繁读写List集合,所以这里使用LinkedList存储数据库连接比较合适
*/
private static LinkedList<Connection> listConnections = new LinkedList<Connection>();

static{
//在静态代码块中加载db.properties数据库配置文件
InputStream in = JdbcPool.class.getClassLoader().getResourceAsStream("db.properties");
Properties prop = new Properties();
try {
prop.load(in);
String driver = prop.getProperty("driver");
String url = prop.getProperty("url");
String username = prop.getProperty("username");
String password = prop.getProperty("password");
//数据库连接池的初始化连接数大小
int jdbcPoolInitSize =Integer.parseInt(prop.getProperty("jdbcPoolInitSize"));
//加载数据库驱动
Class.forName(driver);
for (int i = 0; i < jdbcPoolInitSize; i++) {
Connection conn = DriverManager.getConnection(url, username, password);
System.out.println("获取到了链接" + conn);
//将获取到的数据库连接加入到listConnections集合中,listConnections集合此时就是一个存放了数据库连接的连接池
listConnections.add(conn);
}

} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}

@Override
public PrintWriter getLogWriter() throws SQLException {
// TODO Auto-generated method stub
return null;
}

@Override
public void setLogWriter(PrintWriter out) throws SQLException {
// TODO Auto-generated method stub

}

@Override
public void setLoginTimeout(int seconds) throws SQLException {
// TODO Auto-generated method stub

}

@Override
public int getLoginTimeout() throws SQLException {
// TODO Auto-generated method stub
return 0;
}

@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
// TODO Auto-generated method stub
return null;
}

@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
// TODO Auto-generated method stub
return false;
}

/* 获取数据库连接
* @see javax.sql.DataSource#getConnection()
*/
@Override
public Connection getConnection() throws SQLException {
//如果数据库连接池中的连接对象的个数大于0
if (listConnections.size()>0) {
//从listConnections集合中获取一个数据库连接
final Connection conn = listConnections.removeFirst();
System.out.println("listConnections数据库连接池大小是" + listConnections.size());
//返回Connection对象的代理对象
return (Connection) Proxy.newProxyInstance(JdbcPool.class.getClassLoader(), conn.getClass().getInterfaces(), new InvocationHandler(){
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if(!method.getName().equals("close")){
return method.invoke(conn, args);
}else{
//如果调用的是Connection对象的close方法,就把conn还给数据库连接池
listConnections.add(conn);
System.out.println(conn + "被还给listConnections数据库连接池了!!");
System.out.println("listConnections数据库连接池大小为" + listConnections.size());
return null;
}
}
});
}else {
throw new RuntimeException("对不起,数据库忙");
}
}

@Override
public Connection getConnection(String username, String password)
throws SQLException {
return null;
}
}

开源数据库连接池

开源组织提供了数据源的独立实现:

  • DBCP 数据库连接池
  • C3P0 数据库连接池

DBCP

Tomcat 的连接池正是采用该连接池来实现的。该数据库连接池既可以与应用服务器整合使用,也可由应用程序独立使用。

使用DBCP数据源的步骤:

  1. 导入两个jar包【Commons-dbcp.jar和Commons-pool.jar】

  2. 读取配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    #连接设置
    driverClassName=com.mysql.jdbc.Driver
    url=jdbc:mysql://localhost:3306/jdbcstudy
    username=root
    password=XDP

    #<!-- 初始化连接 -->
    initialSize=10

    #最大连接数量
    maxActive=50

    #<!-- 最大空闲连接 -->
    maxIdle=20

    #<!-- 最小空闲连接 -->
    minIdle=5

    #<!-- 超时等待时间以毫秒为单位 6000毫秒/1000等于60秒 -->
    maxWait=60000


    #JDBC驱动建立连接时附带的连接属性属性的格式必须为这样:[属性名=property;]
    #注意:"user" 与 "password" 两个属性会被明确地传递,因此这里不需要包含他们。
    connectionProperties=useUnicode=true;characterEncoding=UTF8

    #指定由连接池所创建的连接的自动提交(auto-commit)状态。
    defaultAutoCommit=true

    #driver default 指定由连接池所创建的连接的只读(read-only)状态。
    #如果没有设置该值,则“setReadOnly”方法将不被调用。(某些驱动并不支持只读模式,如:Informix)
    defaultReadOnly=

    #driver default 指定由连接池所创建的连接的事务级别(TransactionIsolation)。
    #可用值为下列之一:(详情可见javadoc。)NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
    defaultTransactionIsolation=READ_UNCOMMITTED
  3. 获取BasicDataSourceFactory对象

  4. 创建DataSource对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class JdbcUtils_DBCP {    /**
* 在java中,编写数据库连接池需实现java.sql.DataSource接口,每一种数据库连接池都是DataSource接口的实现
* DBCP连接池就是java.sql.DataSource接口的一个具体实现
*/
private static DataSource dataSource = null;
//在静态代码块中创建数据库连接池
static {
try {
//加载dbcpconfig.properties配置文件
InputStream inputStream = Demo3.class.getClassLoader().getResourceAsStream("dbcpconfig.properties");
Properties properties = new Properties();
properties.load(inputStream);

//获取工厂对象
BasicDataSourceFactory basicDataSourceFactory = new BasicDataSourceFactory();
//创建数据源
dataSource = basicDataSourceFactory.createDataSource(properties);

} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}

public static Connection getConnection() throws SQLException {
//从数据源中获取数据库连接
return dataSource.getConnection();

}

//这里释放资源不是把数据库的物理连接释放了,是把连接归还给连接池【连接池的Connection内部自己做好了】
public static void release(Connection conn, Statement st, ResultSet rs) {

if (rs != null) {
try {
//关闭存储查询结果的ResultSet对象
rs.close();
} catch (Exception e) {
e.printStackTrace();
}
rs = null;
}
if (st != null) {
try {
//关闭负责执行SQL命令的Statement对象
st.close();
} catch (Exception e) {
e.printStackTrace();
}

}
if (conn != null) {
try {
//将Connection连接对象还给数据库连接池
conn.close();
} catch (Exception e) {
e.printStackTrace();
}

}
}

C3P0

C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。目前使用它的开源项目有Hibernate,Spring等。C3P0数据源在项目开发中使用得比较多。

  c3p0与dbcp区别

  1. dbcp没有自动回收空闲连接的功能
  2. c3p0有自动回收空闲连接功能

步骤:

  1. 导入开发包【c3p0-0.9.2-pre1.jar】和【mchange-commons-0.2.jar】
  2. 导入XML配置文件【可以在程序中自己一个一个配,C3P0的doc中的Configuration有XML文件的事例】
  3. new出ComboPooledDataSource对象

 c3p0-config.xml的配置信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version="1.0" encoding="UTF-8"?>
<!--
c3p0-config.xml必须位于类路径下面
private static ComboPooledDataSource ds;
static{
try {
ds = new ComboPooledDataSource("MySQL");
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
-->

<c3p0-config>
<!--
C3P0的缺省(默认)配置,
如果在代码中“ComboPooledDataSource ds = new ComboPooledDataSource();”这样写就表示使用的是C3P0的缺省(默认)配置信息来创建数据源
-->
<default-config>
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/jdbcstudy</property>
<property name="user">root</property>
<property name="password">XDP</property>

<property name="acquireIncrement">5</property>
<property name="initialPoolSize">10</property>
<property name="minPoolSize">5</property>
<property name="maxPoolSize">20</property>
</default-config>

<!--
C3P0的命名配置,
如果在代码中“ComboPooledDataSource ds = new ComboPooledDataSource("MySQL");”这样写就表示使用的是name是MySQL的配置信息来创建数据源
-->
<named-config name="MySQL">
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/jdbcstudy</property>
<property name="user">root</property>
<property name="password">XDP</property>

<property name="acquireIncrement">5</property>
<property name="initialPoolSize">10</property>
<property name="minPoolSize">5</property>
<property name="maxPoolSize">20</property>
</named-config>

</c3p0-config>

在获取数据库连接的工具类(如jdbcUtils)的静态代码块中创建池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class JdbcUtils_C3P0 {

private static ComboPooledDataSource ds = null;
//在静态代码块中创建数据库连接池
static{
try{
//通过代码创建C3P0数据库连接池
/*ds = new ComboPooledDataSource();
ds.setDriverClass("com.mysql.jdbc.Driver");
ds.setJdbcUrl("jdbc:mysql://localhost:3306/jdbcstudy");
ds.setUser("root");
ds.setPassword("XDP");
ds.setInitialPoolSize(10);
ds.setMinPoolSize(5);
ds.setMaxPoolSize(20);*/

//通过读取C3P0的xml配置文件创建数据源,C3P0的xml配置文件c3p0-config.xml必须放在src目录下
//ds = new ComboPooledDataSource();//使用C3P0的默认配置来创建数据源
ds = new ComboPooledDataSource("MySQL");//使用C3P0的命名配置来创建数据源

}catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
public static Connection getConnection() throws SQLException{
//从数据源中获取数据库连接
return ds.getConnection();
}

/**
* 释放的资源包括Connection数据库连接对象,负责执行SQL命令
*/
public static void release(Connection conn,Statement st,ResultSet rs){
if(rs!=null){
try{
//关闭存储查询结果的ResultSet对象
rs.close();
}catch (Exception e) {
e.printStackTrace();
}
rs = null;
}
if(st!=null){
try{
//关闭负责执行SQL命令的Statement对象
st.close();
}catch (Exception e) {
e.printStackTrace();
}
}

if(conn!=null){
try{
//将Connection连接对象还给数据库连接池
conn.close();
}catch (Exception e) {
e.printStackTrace();
}
}
}
}

配置Tomcat数据源

Tomcat服务器也给我们提供了连接池,内部其实就是DBCP

步骤:

  1. 在META-INF目录下配置context.xml文件【文件内容可以在tomcat默认页面的 JNDI Resources下Configure Tomcat’s Resource Factory找到】
  2. 导入Mysql或oracle开发包导入到tomcat的lib目录下
  3. 初始化JNDI->获取JNDI容器->检索以XXX为名字在JNDI容器存放的连接池

context.xml文件的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<Context>

<Resource name="jdbc/EmployeeDB"
auth="Container"
type="javax.sql.DataSource"

username="root"
password="root"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/zhongfucheng"
maxActive="8"
maxIdle="4"/>
</Context>
try {

//初始化JNDI容器
Context initCtx = new InitialContext();

//获取到JNDI容器
Context envCtx = (Context) initCtx.lookup("java:comp/env");

//扫描以jdbc/EmployeeDB名字绑定在JNDI容器下的连接池
DataSource ds = (DataSource)
envCtx.lookup("jdbc/EmployeeDB");

Connection conn = ds.getConnection();
System.out.println(conn);

}

在获取数据库连接的工具类(如jdbcUtils)的静态代码块中获取JNDI容器中的数据源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class JdbcUtils_JNDI {

private static DataSource ds = null;
//在静态代码块中创建数据库连接池
static{
try{
//初始化JNDI
Context initCtx = new InitialContext();
//得到JNDI容器
Context envCtx = (Context) initCtx.lookup("java:comp/env");
//从JNDI容器中检索name为jdbc/datasource的数据源
ds = (DataSource)envCtx.lookup("jdbc/datasource");
}catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}

public static Connection getConnection() throws SQLException{
//从数据源中获取数据库连接
return ds.getConnection();
}
public static void release(Connection conn,Statement st,ResultSet rs){
if(rs!=null){
try{
//关闭存储查询结果的ResultSet对象
rs.close();
}catch (Exception e) {
e.printStackTrace();
}
rs = null;
}
if(st!=null){
try{
//关闭负责执行SQL命令的Statement对象
st.close();
}catch (Exception e) {
e.printStackTrace();
}
}

if(conn!=null){
try{
//将Connection连接对象还给数据库连接池
conn.close();
}catch (Exception e) {
e.printStackTrace();
}
}
}
}

使用dbutils框架

dbutils它是对JDBC的简单封装,极大简化jdbc编码的工作量

commons-dbutilsAPI介绍:

  • org.apache.commons.dbutils.QueryRunner
  • org.apache.commons.dbutils.ResultSetHandler

工具类:

  • org.apache.commons.dbutils.DbUtils

DbUtils类

提供了关闭连接,装载JDBC驱动,回滚提交事务等方法的工具类【比较少使用,因为我们学了连接池,就应该使用连接池连接数据库】

QueryRunner类

该类简化了SQL查询,配合ResultSetHandler使用,可以完成大部分的数据库操作,重载了许多的查询,更新,批处理方法。大大减少了代码量

ResultSetHandler接口

该接口规范了对ResultSet的操作,要对结果集进行什么操作,传入ResultSetHandler接口的实现类即可。

  • ArrayHandler:把结果集中的第一行数据转成对象数组。
  • ArrayListHandler:把结果集中的每一行数据都转成一个数组,再存放到List中。
  • BeanHandler:将结果集中的第一行数据封装到一个对应的JavaBean实例中。
  • BeanListHandler:将结果集中的每一行数据都封装到一个对应的JavaBean实例中,存放到List里。
  • ColumnListHandler:将结果集中某一列的数据存放到List中。
  • KeyedHandler(name):将结果集中的每一行数据都封装到一个Map里,再把这些map再存到一个map里,其key为指定的key。
  • MapHandler:将结果集中的第一行数据封装到一个Map里,key是列名,value就是对应的值。
  • MapListHandler:将结果集中的每一行数据都封装到一个Map里,然后再存放到List
  • ScalarHandler 将ResultSet的一个列到一个对象中。

使用DbUtils框架对数据库的CRUD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* 使用DbUtils框架对数据库的CRUD
* 批处理
*
* */
public class Test {

@org.junit.Test
public void add() throws SQLException {

//创建出QueryRunner对象
QueryRunner queryRunner = new QueryRunner(JdbcUtils.getDataSource());
String sql = "INSERT INTO student (id,name) VALUES(?,?)";

//我们发现query()方法有的需要传入Connection对象,有的不需要传入
//区别:你传入Connection对象是需要你来销毁该Connection,你不传入,由程序帮你把Connection放回到连接池中
queryRunner.update(sql, new Object[]{"100", "zhongfucheng"});

}

@org.junit.Test
public void query()throws SQLException {

//创建出QueryRunner对象

事务

一个SESSION所进行的所有更新操作要么一起成功,要么一起失败

注意:当Connection遇到一个未处理的SQLException时,系统会非正常退出,事务也会自动回滚,但如果程序捕获到了异常,是需要在catch中显式回滚事务的。

savapoint

可以设置中间点。

注意:savepoint不会结束当前事务,普通提交和回滚都会结束当前事务的

注事务的隔离级别

数据库定义了4个隔离级别:

  1. Serializable【可避免脏读,不可重复读,虚读】
  2. Repeatable read【可避免脏读,不可重复读】
  3. Read committed【可避免脏读】
  4. Read uncommitted【级别最低,什么都避免不了】

分别对应Connection类中的4个常量

  1. TRANSACTION_READ_UNCOMMITTED
  2. TRANSACTION_READ_COMMITTED
  3. TRANSACTION_REPEATABLE_READ
  4. TRANSACTION_SERIALIZABLE

脏读:一个事务读取到另外一个事务未提交的数据

不可重复读:一个事务读取到另外一个事务已经提交的数据,也就是说一个事务可以看到其他事务所做的修改

虚读(幻读):是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。

注:和不可重复读类似,但虚读(幻读)会读到其他事务的插入的数据,导致前后读取不一致


简单总结:脏读是不可容忍的,不可重复读和虚读在一定的情况下是可以的【做统计的肯定就不行】

事务处理

  • 在数据访问层(Dao)处理事务

    Dao层只涉及到最基本的CRUD,不涉及具体的业务操作,所以在开发中DAO层出现这样的业务处理方法是一种不好的设计。

  • 在业务层(BusinessService)处理事务

  • 使用ThreadLocal进行事务处理

分页

Oracle实现分页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/*
Oracle分页:
Oracle的分页依赖于ROWNUM这个伪列,ROWNUM主要作用就是产生行号。

分页原理:
1:子查询查出前n行数据,ROWNUM产生前N行的行号
2:使用子查询产生ROWNUM的行号,通过外部的筛选出想要的数据

例子:
我现在规定每页显示5行数据【lineSize=5】,我要查询第2页的数据【currentPage=2】
注:【对照着语法来看】

实现:
1:子查询查出前10条数据【ROWNUM<=10】
2:外部筛选出后面5条数据【ROWNUM>5】
3:这样我们就取到了后面5条的数据
*/
/*
Oracle分页语法:
@lineSize---每页显示数据行数
/*
Oracle分页:
Oracle的分页依赖于ROWNUM这个伪列,ROWNUM主要作用就是产生行号。

分页原理:
1:子查询查出前n行数据,ROWNUM产生前N行的行号
2:使用子查询产生ROWNUM的行号,通过外部的筛选出想要的数据

例子:
我现在规定每页显示5行数据【lineSize=5】,我要查询第2页的数据【currentPage=2】
注:【对照着语法来看】

实现:
1:子查询查出前10条数据【ROWNUM<=10】
2:外部筛选出后面5条数据【ROWNUM>5】
3:这样我们就取到了后面5条的数据
*/

/*
Oracle分页语法:
@lineSize---每页显示数据行数
@currentPage----当前所在页

*/
SELECT *FROM (
SELECT 列名,列名,ROWNUM rn
FROM 表名
WHERE ROWNUM<=(currentPage*lineSize)) temp

WHERE temp.rn>(currentPage-1)*lineSize;

Mysql实现分页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    /*
Mysql分页语法:
@start---偏移量,不设置就是从0开始【也就是(currentPage-1)*lineSize】
@length---长度,取多少行数据

*/
SELECT *
FROM 表名
LIMIT [START], length;

/*
例子:
我现在规定每页显示5行数据,我要查询第2页的数据

分析:
1:第2页的数据其实就是从第6条数据开始,取5条

实现:
1:start为5【偏移量从0开始】
2:length为5

*/

总结:

  • Mysql从(currentPage-1)*lineSize开始取数据,取lineSize条数据
  • Oracle先获取currentPage*lineSize条数据,从(currentPage-1)*lineSize开始取数据

面试题

参考文章

JSTL标签库

为了弥补html标签的不足,规范自定义标签的使用而诞生的。使用JSLT标签的目的就是不希望在jsp页面中出现java逻辑代码。

分类

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

核心标签库

JSTL的核心标签库标签共13个,使用这些标签能够完成JSP页面的基本功能,减少编码工作。

  从功能上可以分为4类:表达式控制标签、流程控制标签、循环标签、URL操作标签。
    (1)表达式控制标签out标签、set标签、remove标签、catch标签。
    (2)流程控制标签if标签、choose标签、when标签、otherwise标签
    (3)循环标签forEach标签、forTokens标签
    (4)URL操作标签import标签、url标签、redirect标签、param标签

  在JSP页面引入核心标签库的代码为:**<%@ taglib prefix=”c” uri=”http://java.sun.com/jsp/jstl/core" %>**

out标签

<c:out>标签主要是用来输出数据对象(字符串、表达式)的内容或结果
  在使用Java脚本输出时常使用的方式为: <% out.println(“字符串”)%> 或者 <%=表达式%> ,在web开发中,为了避免暴露逻辑代码会尽量减少页面中的Java脚本,使用<c:out>标签就可以实现以上功能。

1
2
<c:out value=”字符串”>
<c:out value=”EL表达式”>

  JSTL的使用是和EL表达式分不开的,EL表达式虽然可以直接将结果返回给页面,但有时得到的结果为空,<c:out>有特定的结果处理功能,EL的单独使用会降低程序的易读性,建议把EL的结果输入放入<c:out>标签中。

<c:out>标签的语法:

1
2
<c:out value=”要显示的数据对象” [escapeXml=”true|false”] [default=”默认值”]/>
<c:out value=”要显示的数据对象” [escapeXml=”true|false”]>默认值</c:out>
属性名 是否支持EL 属性类型 属性描述
value true Object 指定要输出的内容
escapeXml true Boolean 指定是否将>、<、&等特殊字符进行HTML编码转换后再进行输出。默认true
default true Object 指定如果value属性值为null时所输出的默认值

使用范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<%@ page language="java" pageEncoding="UTF-8"%>
<%--引入JSTL核心标签库 --%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE HTML>
<html>
<head>
<title>JSTL: --表达式控制标签“out”标签的使用</title>
</head>

<body>
<h3><c:out value="下面的代码演示了c:out的使用,以及在不同属性值状态下的结果。"/></h3>
<hr/>
<ul>
<%--(1)直接输出了一个字符串。 --%>
<li>(1)<c:out value="JSTL的out标签的使用" /></li>

<li>(2)<c:out value="<a href='http://www.cnblogs.com/'>点击链接到博客园</a>" /></li>
<%--escapeXml="false"表示value值中的html标签不进行转义,而是直接输出 --%>
<li>(3)<c:out value="<a href='http://www.cnblogs.com/'>点击链接到博客园</a>" escapeXml="false"/></li>

<%--(4)字符串中有转义字符,但在默认情况下没有转换。 --%>
<li>(4)<c:out value="&lt未使用字符转义&gt" /></li>
<%--(5)使用了转义字符&lt和&gt分别转换成<>符号。 --%>
<li>(5)<c:out value="&lt使用字符转义&gt" escapeXml="false"></c:out></li>

<%--(6)设定了默认值,从EL表达式${null}得到空值,所以直接输出设定的默认值。 --%>
<li>(6)<c:out value="${null}">使用了默认值</c:out></li>
<%--(7)未设定默认值,输出结果为空。 --%>
<li>(7)<c:out value="${null}"></c:out></li>

<%--(8)设定了默认值,从EL表达式${null}得到空值,所以直接输出设定的默认值。 --%>
<li>(8)<c:out value="${null}" default="默认值"/></li>
<%--(9)未设定默认值,输出结果为空。 --%>
<li>(9)<c:out value="${null}"/></li>
</ul>
</body>
</html>

set标签

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

语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1:存值,把一个值放在指定的域范围内。
    <c:set value=”值1” var=”name1” [scope=”page|request|session|application”]/>
    含义:把一个变量名为name1值为“值1”的变量存储在指定的scope范围内。
2:
    <c:set var=”name2” [scope=”page|request|session|application”]>
      值2
    </c:set>
    含义:把一个变量名为name2,值为值2的变量存储在指定的scope范围内。
3:
    <c:set value=”值3” target=”JavaBean对象” property=”属性名”/>
    含义:把一个值为“值3”赋值给指定的JavaBean的属性名。相当与setter()方法。
4:
    <c:set target=”JavaBean对象” property=”属性名”>
      值4
    </c:set>
    含义:把一个值4赋值给指定的JavaBean的属性名。
属性名 是否支持EL 属性类型 属性描述
value true Object 指定要输出的内容
var false String 指定要设置的web域属性的名称
scope false String 指定属性所在的web域
target true Object 指定要设置属性的对象(javabean或是map)
property true String 指定当前为对象设置的属性名称

使用范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%--引入JSTL核心标签库 --%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%--使用JSP的指令元素指定要使用的JavaBean --%>
<jsp:useBean id="person" class="javabean.Person"/>
<%--负责实例化Bean,id指定实例化后的对象名,可以通过${person}得到person在内存中的值
(或者使用person.toString()方法)。 --%>
<!DOCTYPE HTML>
<html>
<head>

<title>JSTL: --表达式控制标签“set”标签的使用</title>

</head>

<body>
<h3>代码给出了给指定scope范围赋值的示例。</h3>
<ul>
<%--通过<c:set>标签将data1的值放入page范围中。--%>
<li>把一个值放入page域中:<c:set var="data1" value="xdp" scope="page"/></li>
<%--使用EL表达式从pageScope得到data1的值。--%>
<li>从page域中得到值:${pageScope.data1}</li>

<%--通过<c:set>标签将data2的值放入request范围中。--%>
<li>把一个值放入request域中:<c:set var="data2" value="gacl" scope="request"/></li>
<%--使用EL表达式从requestScope得到data2的值。--%>
<li>从request域中得到值:${requestScope.data2}</li>

<%--通过<c:set>标签将值name1的值放入session范围中。--%>
<li>把一个值放入session域中。<c:set value="孤傲苍狼" var="name1" scope="session"></c:set></li>
<%--使用EL表达式从sessionScope得到name1的值。--%>
<li>从session域中得到值:${sessionScope.name1} </li>

<%--把name2放入application范围中。 --%>
<li>把一个值放入application域中。<c:set var="name2" scope="application">白虎神皇</c:set></li>
<%--使用EL表达式从application范围中取值,用<c:out>标签输出使得页面规范化。 --%>
<li>使用out标签和EL表达式嵌套从application域中得到值:
<c:out value="${applicationScope.name2}">未得到name的值</c:out>
</li>

<%--不指定范围使用EL自动查找得到值 --%>
<li>未指定scope的范围,会从不同的范围内查找得到相应的值:${data1}、${data2}、${name1}、${name2}</li>
</ul>
<hr/>
<h3>使用Java脚本实现以上功能</h3>
<ul>

<li>把一个值放入page域中。<%pageContext.setAttribute("data1","xdp");%></li>
<li>从page域中得到值:<%out.println(pageContext.getAttribute("data1"));%></li>

<li>把一个值放入request域中。<%request.setAttribute("data2","gacl");%></li>
<li>从request域中得到值:<%out.println(request.getAttribute("data2"));%></li>

<li>把一个值放入session域中。<%session.setAttribute("name1","孤傲苍狼");%></li>
<li>从session中域得到值:<%out.println(session.getAttribute("name1"));%></li>
<%--out.println()方法与<%=%>表达式输出功能一样
但使用表达式输出(<%=%>)明显要比使用out.println()输出更好。
--%>
<li><%=session.getAttribute("name1") %></li>
<li>把另一个值放入application域中。<%application.setAttribute("name2","白虎神皇");%></li>
<li> 从application域中得到值:<%out.println(application.getAttribute("name2"));%></li>
<li><%=application.getAttribute("name2")%></li>

<li>未指定scope的范围,会从不同的范围内查找得到相应的值:
<%=pageContext.findAttribute("data1")%>、
<%=pageContext.findAttribute("data2")%>、
<%=pageContext.findAttribute("name1")%>、
<%=pageContext.findAttribute("name2")%>
</li>

</ul>
<hr/>
<h3>操作JavaBean,设置JavaBean的属性值</h3>
<%--设置JavaBean的属性值,等同与setter方法,Target指向实例化后的对象,property指向要插入值的参数名。
注意:使用target时一定要指向实例化后的JavaBean对象,也就是要跟<jsp:useBean>配套使用,
也可以java脚本实例化,但这就失去了是用标签的本质意义。
使用Java脚本实例化:
<%@page import="javabean.Person"%
<% Person person=new Person(); %>
--%>
<c:set target="${person}" property="name">孤傲苍狼</c:set>
<c:set target="${person}" property="age">25</c:set>
<c:set target="${person}" property="sex"></c:set>
<c:set target="${person}" property="home">中国</c:set>
<ul>
<li>使用的目标对象为:${person}</li>
<li>从Bean中获得的name值为:<c:out value="${person.name}"></c:out></li>
<li>从Bean中获得的age值为:<c:out value="${person.age}"></c:out></li>
<li>从Bean中获得的sex值为:<c:out value="${person.sex}"></c:out></li>
<li>从Bean中获得的home值为:<c:out value="${person.home}"></c:out></li>
</ul>
<hr/>
<h3>操作Map</h3>
<%
Map map = new HashMap();
request.setAttribute("map",map);
%>
<%--将data对象的值存储到map集合中 --%>
<c:set property="data" value="gacl" target="${map}"/>
${map.data}
</body>
</html>

remove标签

从指定的JSP范围内移除指定的变量。

语法:

1
2
<c:remove var=”变量名” [scope=”page|request|session|application”]/>
  其中var属性是必须的,scope可以以省略。

范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%--引入JSTL核心标签库 --%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE HTML>
<html>
<head>
<title>JSTL: --表达式控制标签“remove”标签的使用</title>
</head>

<body>
<ul>
<c:set var="name" scope="session">孤傲苍狼</c:set>
<c:set var="age" scope="session">25</c:set>
<li><c:out value="${sessionScope.name}"></c:out></li>
<li><c:out value="${sessionScope.age}"></c:out></li>
<%--使用remove标签移除age变量 --%>
<c:remove var="age" />
<li><c:out value="${sessionScope.name}"></c:out></li>
<li><c:out value="${sessionScope.age}"></c:out></li>
</ul>
</body>
</html>

catch标签

用于捕获嵌套在标签体中的内容抛出的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%--引入JSTL核心标签库 --%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE HTML>
<html>
<head>
<title>JSTL: --表达式控制标签“catch”标签实例</title>
</head>

<body>
<h4>catch标签实例</h4>
<hr>
<%--把容易产生异常的代码放在<c:catch></c:catch>中,
自定义一个变量errorInfo用于存储异常信息 --%>
<c:catch var="errorInfo">
<%--实现了一段异常代码,向一个不存在的JavaBean中插入一个值--%>
<c:set target="person" property="hao"></c:set>
</c:catch>
<%--用EL表达式得到errorInfo的值,并使用<c:out>标签输出 --%>
异常:<c:out value="${errorInfo}" /><br />
异常 errorInfo.getMessage:<c:out value="${errorInfo.message}" /><br />
异常 errorInfo.getCause:<c:out value="${errorInfo.cause}" /><br />
异常 errorInfo.getStackTrace:<c:out value="${errorInfo.stackTrace}" />
</body>
</html>

其他的标签可见:
javaweb学习总结(二十八)——JSTL标签库之核心标签 - 孤傲苍狼 - 博客园 (cnblogs.com)

JSP+JavaBean开发模式

img

JSP负责控制逻辑、表现逻辑、业务对象(javabean)的调用。

JSP+JavaBean模式适合开发业务逻辑不太复杂的web应用程序,这种模式下,JavaBean用于封装业务数据,JSP即负责处理用户请求,又显示数据。

Servlet+JSP+JavaBean开发模式

Servlet+JSP+JavaBean组合开发就是一种MVC开发模式了,控制器(Controller)采用Servlet、模型(Model)采用JavaBean、视图(View)采用JSP。

javaweb学习总结(二十一)——JavaWeb的两种开发模式 - 孤傲苍狼 - 博客园 (cnblogs.com)

JSP简介

JSP是为了简化Servlet的工作出现的替代品,Servlet输出HTML非常困难,JSP就是替代Servlet输出HTML的。

JSP的响应

  • 浏览器第一次请求1.jsp时,Tomcat会将1.jsp转化成1_jsp.java这么一个类,并将该文件编译成class文件。编译完毕后再运行class文件来响应浏览器的请求。

  • 以后访问1.jsp就不再重新编译jsp文件了,直接调用class文件来响应浏览器。当然了,如果Tomcat检测到JSP页面改动了的话,会重新编译的。

  • 既然JSP是一个Servlet,那JSP页面中的HTML排版标签是怎么样被发送到浏览器的?用write()出去的。说到底,JSP就是封装了Servlet的java程序罢了。

  • JSP比Servlet更方便更简单的一个重要原因就是:内置了9个对象!内置对象有:out、session、response、request、config、page、application、pageContext、exception

生命周期

JSP也是Servlet,运行时只有一个实例,JSP初始化和销毁时也会调用Servlet的init()和destroy()方法。另外,JSP还有自己初始化和销毁的方法

1
2
3
4
5
6
7
public void _jspInit() {
_el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory();
_jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig());
}

public void _jspDestroy() {
}

语法

JSP代码分为两个部分:

  1. 模板数据:HTML代码
    • 定义了网页的基本骨架————页面的结构和外观
  2. 元素:JSP页面中的java代码、JSP指令和JSP标签

脚本

  • JSP的脚本就是JSP页面中的java代码,也叫做scriptlet。JSP的脚本必须使用<%%>括起来,不然会被当成是模板数据的!
  • JSP脚本有三种方式:
    • <%%>【定义局部变量,编写语句】
    • <%!%>【定义类或方法,但是没人这样用!
    • <%=%>(也称之为表达式输出)【输出各种类型的变量,int、double、String、Object等】
    • 表达式和变量后面不能有分号(;)
    • 在<% %>中可以定义变量、编写语句,不能定义方法。
  • 如果过多地使用<%%>会导致代码混乱,JSP还提供了一种scriptlet标签,使用此标签和<%%>有相同的功能,只不过它更美观了一些
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 <jsp:scriptlet>

String s = "HelloWorld";
out.println(s);

</jsp:scriptlet>

<%--这是JSP注释--%>
<%--%>
<%
int sum=0;//声明变量

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

指令

JSP指令用来声明JSP页面的相关属性,例如编码方式、文档类型等等

1
<%@指令  属性名="值"  %>

page指令

page指令可以帮助跳到提示页面或是错误页面上。为了保持程序的可读性和遵循良好的编程习惯,page指令最好是放在整个JSP页面的起始位置。

  • 我在idea生成的JSP页面就有page指令了。
1
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
  • page指令常见属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%@ page
[ language="java" ]
[ extends="package.class" ]
[ import="{package.class | package.*}, ..." ]
[ session="true | false" ]
[ buffer="none | 8kb | sizekb" ]
[ autoFlush="true | false" ]
[ isThreadSafe="true | false" ]
[ info="text" ]
[ errorPage="relative_url" ]【通过page指令的errorPage属性跳转到relative_url页面】
[ isErrorPage="true | false" ]【设置页面是否是错误页面】
[ contentType="mimeType [ ;charset=characterSet ]" | "text/html ; charset=ISO-8859-1" ]
[ pageEncoding="characterSet | ISO-8859-1" ]
[ isELIgnored="true | false" ]
%>

一般地,在eclipse或idea这些高级开发工具上开发,我们只需要在page指令中指定contentType=”text/html;charset=UTF-8”,就不会出现中文乱码问题!当然了contentType 不仅仅可以指定以text/html的方式显示,还可以使用其他的形式显示出来。在conf/web.xml文件中可以查询出来

import属性

  • 可以在一条page指令的import属性中引入多个类或包,其中的每个包或类之间使用逗号(,)分隔

  • ```jsp
    <%@ page import=”java.util.*,java.io.*,java.sql.*”%>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36

    ### errorPage属性

    - rrorPage属性的设置值必须使用相对路径,如果以“/”开头,表示相对于当前Web应用程序的根目录(注意不是站点根目录),否则,表示相对于当前页面

    - 可以在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\>/ErrorPage/404Error.jsp\</location\>

    - 如果设置了某个JSP页面的errorPage属性,那么在web.xml文件中设置的错误处理将不对该页面起作用。

    web.xml的代码下:

    ```xml
    <?xml version="1.0" encoding="UTF-8"?>
    <web-app version="3.0"
    xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
    http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
    <display-name></display-name>
    <welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>

    <!-- 针对404错误的处理页面 -->
    <error-page>
    <error-code>404</error-code>
    <location>/ErrorPage/404Error.jsp</location>
    </error-page>

    </web-app>

    404Error.jsp代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
    <html>
    <head>
    <title>404错误友好提示页面</title>
    <!-- 3秒钟后自动跳转回首页 -->
    <meta http-equiv="refresh" content="3;url=${pageContext.request.contextPath}/index.jsp">
    </head>
    <body>
    <img alt="对不起,你要访问的页面没有找到,请联系管理员处理!"
    src="${pageContext.request.contextPath}/img/404Error.png"/><br/>
    3秒钟后自动跳转回首页,如果没有跳转,请点击<a href="${pageContext.request.contextPath}/index.jsp">这里</a>
    </body>
    </html>

include指令

在JSP中对于包含有两种语句形式:

  1. @include指令
  2. jsp:include指令

include指令是静态包含。静态包含的意思就是:把文件的代码内容都包含进来,再编译!

include指令细节注意问题:

  1. 被引入的文件必须遵循JSP语法。
  2. 被引入的文件可以使用任意的扩展名,即使其扩展名是html,JSP引擎也会按照处理jsp页面的方式处理它里面的内容,为了见明知意,JSP规范建议使用.jspf(JSP fragments(片段))作为静态引入文件的扩展名。
  3. 由于使用include指令将会涉及到2个JSP页面,并会把2个JSP翻译成一个servlet,所以这2个JSP页面的指令不能冲突(除了pageEncoding和导包除外)。

在1.jsp中把页头和页尾包含进来:

1
2
3
4
5
6
7
8
9
10
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>包含页头和页尾进来</title>
</head>
<body>
<%@include file="head.jsp" %>
<%@include file="foot.jsp" %>
</body>
</html>

taglib指令

用来说明JSP页面内使用的标签库

行为(内置标签)

JSP行为(JSP Actions)是一组JSP内置的标签,只书写少量的标记代码就能够使用JSP提供丰富的功能,JSP行为是对常用的JSP功能的抽象和封装

 jsp的常用标签有以下三个

include标签

  • include标签语法是这个样子的
1
2
3
<jsp:include page="relativeURL | <%=expression%>" flush="true|false" />
  page属性用于指定被引入资源的相对路径,它也可以通过执行一个表达式来获得。
  flush属性指定在插入其他资源的输出内容时,是否先将当前JSP页面的已输出的内容刷新到客户端。
  • jsp标签包含文件就是先编译被包含的页面,再将页面的结果写入到包含的页面中————属于动态包含

include标签和include指令的区别

<jsp:include>标签是动态引入, <jsp:include>标签涉及到的2个JSP页面会被翻译成2个servlet,这2个servlet的内容在执行时进行合并。
  而include指令是静态引入,涉及到的2个JSP页面会被翻译成一个servlet,其内容是在源文件级别进行合并。

param标签

  • 当使用<jsp:include>和<jsp:forward>标签引入或将请求转发给其它资源时,可以使用<jsp:param>标签向这个资源传递参数。

forward标签

<jsp:forward>标签用于把请求转发给另外一个资源。

  • forward标签就是对request.getRequestDispatcher(String url).forward(request,response)的封装

  • 语法:

    1
    2
       <jsp:forward page="relativeURL | <%=expression%>" />
      page属性用于指定请求转发到的资源的相对路径,它也可以通过执行一个表达式来获得。

directive标签

  • directive的中文意思就是指令该行为就是替代指令<%@%>的语法的
    • <jsp:directive.include file=””/> 相当于<%@include file=”” %>
    • <jsp:directive.page/> 相当于<%@page %>
    • <jsp:directive.taglib/> 相当于<%@taglib %>

内置对象

每个JSP 页面在第一次被访问时,WEB容器都会把请求交给JSP引擎(即一个Java程序)去处理。JSP引擎先将JSP翻译成一个_jspServlet(实质上也是一个servlet) ,然后按照servlet的调用方式进行调用。
  由于JSP第一次访问时会翻译成servlet,所以第一次访问通常会比较慢,但第二次访问,JSP引擎如果发现JSP没有变化,就不再翻译,而是直接调用,所以程序的执行效率不会受到影响。
  JSP引擎在调用JSP对应的_jspServlet时,会传递或创建9个与web开发相关的对象供_jspServlet使用。JSP技术的设计者为便于开发人员在编写JSP页面时获得这些web对象的引用,特意定义了9个相应的变量,开发人员在JSP页面中通过这些变量就可以快速获得这9大对象的引用。

九个内置对象:

NO. 内置对象 类型
1 pageContext javax.servlet.jsp.PageContext
2 request javax.servlet.http.HttpServletRequest
3 response javax.servlet.http.HttpServletResponse
4 session javax.servlet.http.HttpSession
5 application javax.servlet.ServletContext
6 config javax.servlet.ServletConfig
7 out javax.servlet.jsp.JspWriter
8 page java.lang.Object
9 exception java.lang.Throwable

————无需定义,即可直接使用

out对象

out对象的API

1
2
3
4
5
6
7
8
int getBufferSize()【得到缓存大小】
int getRemaining()【得到未使用缓存的大小】
boolean isAutoFlush()
void println()
void flush()
void close()
void clearBuffer()
void clear()

out对象用于向客户端发送文本数据。

out对象是通过调用pageContext对象的getOut方法返回的,其作用和用法与ServletResponse.getWriter方法返回的PrintWriter对象非常相似。

JSP页面中的out对象的类型为JspWriter,JspWriter相当于一种带缓存功能的PrintWriter,设置JSP页面的page指令的buffer属性可以调整它的缓存大小,甚至关闭它的缓存。

只有向out对象中写入了内容,且满足如下任何一个条件时,out对象才去调用ServletResponse.getWriter方法,并通过该方法返回的PrintWriter对象将out对象的缓冲区中的内容真正写入到Servlet引擎提供的缓冲区中:

  • 设置page指令的buffer属性关闭了out对象的缓存功能
  • out对象的缓冲区已满
  • 整个JSP页面结束

out对象的原理如下:

  • 只有向out对象中写入了内容,且满足如下任何一个条件时,out对象才去调用ServletResponse.getWriter方法,并通过该方法返回的PrintWriter对象将out对象的缓冲区中的内容真正写入到Servlet引擎提供的缓冲区中
    • 设置page指令的buffer属性关闭了out对象的缓存功能
    • out对象的缓冲区已满
    • 整个JSP页面结束
  • 一般我们在JSP页面输出都是用表达式(<%=%>),所以out对象用得并不是很多

page对象

内置对象page是HttpJasPage对象,其实page对象代表的就是当前JSP页面,是当前JSP编译后的Servlet类的对象。也就是说:page对象相当于普通java类的this

在开发中几乎不用

exception对象

  • 内置对象exception是java.lang.Exception类的对象,exception封装了JSP页面抛出的异常信息。exception经常被用来处理错误页面

pageContext对象

pageContext是内置对象中最重要的一个对象,它代表着JSP页面编译后的内容(也就是JSP页面的运行环境)!

pageContext获取8个内置对象

既然它代表了JSP页面编译后的内容,理所当然的:它封装了对其他8大内置对象的引用!,也就是说,通过pageContext可以获取到其他的8个内置对象!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>获取八大内置对象</title>
</head>
<body>
<%

System.out.println(pageContext.getSession());
System.out.println(pageContext.getRequest());
System.out.println(pageContext.getResponse());

System.out.println(pageContext.getException());

System.out.println(pageContext.getPage());
System.out.println(pageContext.getServletConfig());
System.out.println(pageContext.getServletContext());
System.out.println(pageContext.getOut());

%>

</body>
</html>

pageContext作为域对象

类似于request,session,ServletContext作为域对象而言都有以下三个方法

1
2
3
4
1、setAttribute(String name,Objcet o)
2、getAttribute(String name)
3、removeAttribute(String name)

当然了,pageContext也不例外,pageContext也有这三个方法

pageContext本质上代表的是当前JSP页面编译后的内容,作为域对象而言,它就代表着当前JSP页面(也就是page)!也就是说:pageContext域对象只在page范围内有效,超出了page范围就无效了

findAttribute(String name)方法介绍:

该方法是用于查找各个域中的属性的。当要查找某个属性时,findAttribute方法按照查找顺序”page→request→session→application”在这四个对象中去查找,只要找到了就返回属性值,如果四个对象都没有找到要查找的属性,则返回一个null。

pageContext对象中封装了访问其它域的方法:

1
2
3
1 public java.lang.Object getAttribute(java.lang.String name,int scope)
2 public void setAttribute(java.lang.String name, java.lang.Object value,int scope)
3 public void removeAttribute(java.lang.String name,int scope)

代表各个域的常量

1
2
3
4
1 PageContext.APPLICATION_SCOPE
2 PageContext.SESSION_SCOPE
3 PageContext.REQUEST_SCOPE
4 PageContext.PAGE_SCOPE

PageContext引入和跳转到其他资源

PageContext类中定义了一个forward方法(用来跳转页面)和两个include方法(用来引入页面)来分别简化和替代RequestDispatcher.forward方法和include方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
<%@page contentType="text/html;charset=UTF-8"%>
<%@page import="java.util.*"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<head>
<title>使用pageContext的forward方法跳转页面</title>
</head>
<%
//使用pageContext的forward方法跳转到pageContextDemo05.jsp页面,/代表了当前的web应用
pageContext.forward("/pageContextDemo05.jsp");
//使用pageContext.forward(relativeUrlPath)替代RequestDispatcher.forward(relativeUrlPath)
//使用RequestDispatcher的forward方法实现的跳转方式
//pageContext.getRequest().getRequestDispatcher("/pageContextDemo05.jsp").forward(request, response);
%>

使用include方法引入资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<head>
<title>使用pageContext的include方法引入资源</title>
</head>
<%
pageContext.include("/jspfragments/head.jsp");
%>
使用pageContext的include方法引入资源
<%
pageContext.include("/jspfragments/foot.jsp");
%>
<hr/>
<%--
<jsp:include page="/jspfragments/head.jsp"/>
使用jsp:include标签引入资源
<jsp:include page="/jspfragments/foot.jsp"/>
--%>

其他对象

request对象

  • 内置对象request其实就是HttpServletRequest

response对象

  • 内置对象response其实就是HttpServletResponse

config对象

  • 内置对象config其实就是ServletConfig

session对象

  • 内置对象session其实就是HttpSession。

注意:在page指令配置如下信息,session将不可使用

1
<%@page session="false" %>

application对象

  • 内置对象application其实就是ServletContext对象

四种属性范围

  1. page【只在一个页面中保存属性,跳转页面无效】
  2. request【只在一次请求中保存属性,服务器跳转有效,浏览器跳转无效】
  3. session【在一个会话范围中保存属性,无论何种跳转均有效,关闭浏览器后无效】
  4. application【在整个服务器中保存,所有用户都可以使用】

4个内置对象都支持以下的方法:

No. 方法 描述
1 public void setAttribute(String name,Object value) 设置属性
2 public object getAttribute(String name) 取得属性
3 public void removeAttribute(String name) 删除属性

使用场景

 1、request:如果客户向服务器发请求,产生的数据,用户看完就没用了,像这样的数据就存在request域,像新闻数据,属于用户看完就没用的。
  2、session:如果客户向服务器发请求,产生的数据,用户用完了等一会儿还有用,像这样的数据就存在session域中,像购物数据,用户需要看到自己购物信息,并且等一会儿,还要用这个购物数据结帐。
  3、application(servletContext):如果客户向服务器发请求,产生的数据,用户用完了,还要给其它用户用,像这样的数据就存在application(servletContext)域中,像聊天数据。

JavaBean

JavaBean遵循着特定的写法,通常有以下的规则:

  • 有无参的构造函数
  • 成员属性私有化
  • 封装的属性如果需要被外所操作,必须编写public类型的setter、getter方法

————就是普通的java类。

在jsp中使用javabean

  • <jsp:useBean>【在JSP页面中查找javaBean对象或者实例化javaBean对象】
  • <jsp:setProperty>【设置javaBean的属性】
  • <jsp:getProperty>【获取javaBean的属性】

jsp:useBean

  • <jsp:useBean>标签用于在指定的域范围内查找指定名称的JavaBean对象

    • 存在则直接返回该JavaBean对象的引用
    • 不存在则实例化一个新的JavaBean对象并将它以指定的名称存储到指定的域范围中
  • 语法:

1
2
3
4
<jsp:useBean id="beanName" class="package.class" scope="page|request|session|application"/>
  "id"属性用于指定JavaBean实例对象的引用名称和其存储在域范围中的名称。
  "class"属性用于指定JavaBean的完整类名(即必须带有包名)。
  "scope"属性用于指定JavaBean实例对象所存储的域范围,其取值只能是page、request、session和application等四个值中的一个,其默认值是page。
  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    如果JSP不支持`<jsp:useBean>`这个行为,我们要使用Person类是这样使用的:
    <%--这里需要导入Person类--%>
    <%@ page import="domain.Person" %>

    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
    <title></title>
    </head>
    <body>
    <%
    //new出对象
    Person person = new Person();

    person.setName("zhongfucheng");
    System.out.println(person.getName());
    %>

    </body>
    </html>
    使用<jsp:useBean>:
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
    <title></title>
    </head>
    <body>

    <jsp:useBean id="person" class="domain.Person" scope="page"/>
    <%
    person.setName("zhongfucheng");
    System.out.println(person.getName());
    %>
    </body>
    </html>

jsp:setProperty

  • 语法:
1
<jsp:setProerty name="对象名称" property="属性名" param="参数名" value="值">
  • 在语法上可分为4种模式

    • <jsp:setProperty name=”对象名称” property=”*“/>自动匹配
    • <jsp:setProperty name=”对象名称” property=”属性名称”/>指定属性
    • <jsp:setProperty name=”对象名称” property=”属性名称” param=”参数名称”/>指定参数【很少用】
    • <jsp:setProperty name=”对象名称” property=”属性名称” value=”内容”/>指定内容【很少用】
  • 当我们没有学习到<jsp:setProperty>时,我们获取表单的信息,然后导入到javaBean对象中是这样的一种情况:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    //表单页面代码:

    <form action="/zhongfucheng/1.jsp" method="post">
    用户名:<input type="text" name="username">
    年龄:<input type="text " name="age">
    <input type="submit" value="提交">
    </form>

    //处理表单提交过来数据的jsp的代码:


    <jsp:useBean id="person" class="domain.Person" scope="page"/>
    <%
    int age = Integer.parseInt(request.getParameter("age"));

    person.setAge(age);

    System.out.println(person.getAge());

    %>

    //使用<jsp:setProperty>:


    <jsp:useBean id="person" class="domain.Person" scope="page"/>

    <%--指定属性名称为age--%>
    <jsp:setProperty name="person" property="age"/>
    <%
    System.out.println(person.getAge());
    %>


jsp:getProperty

<jsp:getProperty>标签用于读取JavaBean对象的属性,也就是调用JavaBean对象的getter方法,然后将读取的属性值转换成字符串后插入进输出的响应正文中。

  • 语法:

    1
    <jsp:getProperty name="对象名" property="属性名"/>

EL表法式

主要用于读取数据,进行内容的显示。

语法:

1
${标识符}

EL表达式如果找不到相应的对象属性,返回的的空白字符串“”,而不是null,这是EL表达式最大的特点

获取数据

EL表达式语句在执行时,会调用pageContext.findAttribute方法,用标识符为关键字,分别从page、request、session、application四个域中查找相应的对象,找到则返回相应对象,找不到则返回”” (注意,不是null,而是空字符串)。

  EL表达式可以很轻松获取JavaBean的属性,或获取数组、Collection、Map类型集合的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@page import="me.gacl.domain.Person"%>
<%@page import="me.gacl.domain.Address"%>
<!DOCTYPE HTML>
<html>
<head>
<title>el表达式获取数据</title>
</head>

<body>
<%
request.setAttribute("name","孤傲苍狼");
%>
<%--${name}等同于pageContext.findAttribute("name") --%>
使用EL表达式获取数据:${name}
<hr>
<!-- 在jsp页面中,使用el表达式可以获取bean的属性 -->
<%
Person p = new Person();
p.setAge(12);
request.setAttribute("person",p);
%>
使用el表达式可以获取bean的属性:${person.age}
<hr>
<!-- 在jsp页面中,使用el表达式可以获取bean中的。。。。。。。。。的属性 -->
<%
Person person = new Person();
Address address = new Address();
person.setAddress(address);

request.setAttribute("person",person);
%>
${person.address.name}
<hr>
<!-- 在jsp页面中,使用el表达式获取list集合中指定位置的数据 -->
<%
Person p1 = new Person();
p1.setName("孤傲苍狼");

Person p2 = new Person();
p2.setName("白虎神皇");

List<Person> list = new ArrayList<Person>();
list.add(p1);
list.add(p2);

request.setAttribute("list",list);
%>

<!-- 取list指定位置的数据 -->
${list[1].name}

<!-- 迭代List集合 -->
<c:forEach var="person" items="${list}">
${person.name}
</c:forEach>
<hr>
<!-- 在jsp页面中,使用el表达式获取map集合的数据 -->
<%
Map<String,String> map = new LinkedHashMap<String,String>();
map.put("a","aaaaxxx");
map.put("b","bbbb");
map.put("c","cccc");
map.put("1","aaaa1111");
request.setAttribute("map",map);
%>

<!-- 根据关键字取map集合的数据 -->
${map.c}
${map["1"]}
<hr>
<!-- 迭代Map集合 -->
<c:forEach var="me" items="${map}">
${me.key}=${me.value}<br/>
</c:forEach>
<hr>
</body>
</html>

执行运算

支持关系运算符和逻辑运算符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@page import="me.gacl.domain.User"%>
<!DOCTYPE HTML>
<html>
<head>
<title>el表达式运算符</title>
</head>

<body>
<h3>el表达式进行四则运算:</h3>
加法运算:${365+24}<br/>
减法运算:${365-24}<br/>
乘法运算:${365*24}<br/>
除法运算:${365/24}<br/>

<h3>el表达式进行关系运算:</h3>
<%--${user == null}和 ${user eq null}两种写法等价--%>
${user == null}<br/>
${user eq null}<br/>

<h3>el表达式使用empty运算符检查对象是否为null(空)</h3>
<%

List<String> list = new ArrayList<String>();
list.add("gacl");
list.add("xdp");
request.setAttribute("list",list);
%>
<%--使用empty运算符检查对象是否为null(空) --%>
<c:if test="${!empty(list)}">
<c:forEach var="str" items="${list}">
${str}<br/>
</c:forEach>
</c:if>
<br/>
<%
List<String> emptyList = null;
%>
<%--使用empty运算符检查对象是否为null(空) --%>
<c:if test="${empty(emptyList)}">
对不起,没有您想看的数据
</c:if>

<br/>

<h3>EL表达式中使用二元表达式</h3>
<%
session.setAttribute("user",new User("孤傲苍狼"));
%>
${user==null? "对不起,您没有登陆 " : user.username}

<br/>

<h3>EL表达式数据回显</h3>
<%
User user = new User();
user.setGender("male");
//数据回显
request.setAttribute("user",user);
%>
<input type="radio" name="gender" value="male" ${user.gender=='male'?'checked':''}>
<input type="radio" name="gender" value="female" ${user.gender=='female'?'checked':''}>
<br/>65 </body>
</html>

EL表达式11个内置对象

EL表达式主要是来对内容的显示,为了显示的方便,EL表达式提供了11个内置对象。

  1. pageContext 对应于JSP页面中的pageContext对象(注意:取的是pageContext对象)
  2. pageScope 代表page域中用于保存属性的Map对象
  3. requestScope 代表request域中用于保存属性的Map对象
  4. sessionScope 代表session域中用于保存属性的Map对象
  5. applicationScope 代表application域中用于保存属性的Map对象
  6. param 表示一个保存了所有请求参数的Map对象
  7. paramValues表示一个保存了所有请求参数的Map对象,它对于某个请求参数,返回的是一个string[]
  8. header 表示一个保存了所有http请求头字段的Map对象
  9. headerValues同上,返回string[]数组。
  10. cookie 表示一个保存了所有cookie的Map对象
  11. initParam 表示一个保存了所有web应用初始化参数的map对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<%--pageContext内置对象--%>
<%
pageContext.setAttribute("pageContext1", "pageContext");
%>
pageContext内置对象:${pageContext.getAttribute("pageContext1")}
<br>

<%--pageScope内置对象--%>
<%
pageContext.setAttribute("pageScope1","pageScope");
%>
pageScope内置对象:${pageScope.pageScope1}
<br>

<%--requestScope内置对象--%>
<%
request.setAttribute("request1","reqeust");
%>
requestScope内置对象:${requestScope.request1}
<br>

<%--sessionScope内置对象--%>
<%
session.setAttribute("session1", "session");
%>
sessionScope内置对象:${sessionScope.session1}
<br>

<%--applicationScope内置对象--%>
<%
application.setAttribute("application1","application");
%>
applicationScopt内置对象:${applicationScope.application1}
<br>

<%--header内置对象--%>
header内置对象:${header.Host}
<br>

<%--headerValues内置对象,取出第一个Cookie--%>
headerValues内置对象:${headerValues.Cookie[0]}
<br>


<%--Cookie内置对象--%>
<%
Cookie cookie = new Cookie("Cookie1", "cookie");
%>
Cookie内置对象:${cookie.JSESSIONID.value}
<br>

<%--initParam内置对象,需要为该Context配置参数才能看出效果【jsp配置的无效!亲测】--%>

initParam内置对象:${initParam.name}

<br>


自定义标签

会话技术

基本概念: 指用户开一个浏览器,访问一个网站,只要不关闭该浏览器,不管该用户点击多少个超链接,访问多少资源,直到用户关闭浏览器,整个这个过程我们称为一次会话.

Cookie

Cookie是由W3C组织提出,最早由netscape社区发展的一种机制

  • Cookie的流程:浏览器访问服务器,如果服务器需要记录该用户的状态,就使用response向浏览器发送一个Cookie,浏览器会把Cookie保存起来。当浏览器再次访问服务器的时候,浏览器会把请求的网址连同Cookie一同交给服务器

 Java中的javax.servlet.http.Cookie类用于创建一个Cookie

Cookie类的主要方法
No. 方法 类型 描述
1 [Cookie](https://www.cnblogs.com/xdp-gacl/p/3803033.html#Cookie(java.lang.String, java.lang.String))(String name, String value) 构造方法 实例化Cookie对象,传入cooke名称和cookie的值
2 public String getName() 普通方法 取得Cookie的名字
3 public String getValue() 普通方法 取得Cookie的值
4 public void setValue(String newValue) 普通方法 设置Cookie的值
5 public void setMaxAge(int expiry) 普通方法 设置Cookie的最大保存时间,即cookie的有效期,当服务器给浏览器回送一个cookie时,如果在服务器端没有调用setMaxAge方法设置cookie的有效期,那么cookie的有效期只在一次会话过程中有效,用户开一个浏览器,点击多个超链接,访问服务器多个web资源,然后关闭浏览器,整个过程称之为一次会话,当用户关闭浏览器,会话就结束了,此时cookie就会失效,如果在服务器端使用setMaxAge方法设置了cookie的有效期,比如设置了30分钟,那么当服务器把cookie发送给浏览器时,此时cookie就会在客户端的硬盘上存储30分钟,在30分钟内,即使浏览器关了,cookie依然存在,在30分钟内,打开浏览器访问服务器时,浏览器都会把cookie一起带上,这样就可以在服务器端获取到客户端浏览器传递过来的cookie里面的信息了,这就是cookie设置maxAge和不设置maxAge的区别,不设置maxAge,那么cookie就只在一次会话中有效,一旦用户关闭了浏览器,那么cookie就没有了,那么浏览器是怎么做到这一点的呢,我们启动一个浏览器,就相当于启动一个应用程序,而服务器回送的cookie首先是存在浏览器的缓存中的,当浏览器关闭时,浏览器的缓存自然就没有了,所以存储在缓存中的cookie自然就被清掉了,而如果设置了cookie的有效期,那么浏览器在关闭时,就会把缓存中的cookie写到硬盘上存储起来,这样cookie就能够一直存在了。
6 public int getMaxAge() 普通方法 获取Cookies的有效期
7 public void setPath(String uri) 普通方法 设置cookie的有效路径,比如把cookie的有效路径设置为”/xdp”,那么浏览器访问”xdp”目录下的web资源时,都会带上cookie,再比如把cookie的有效路径设置为”/xdp/gacl”,那么浏览器只有在访问”xdp”目录下的”gacl”这个目录里面的web资源时才会带上cookie一起访问,而当访问”xdp”目录下的web资源时,浏览器是不带cookie的
8 public String getPath() 普通方法 获取cookie的有效路径
9 public void setDomain(String pattern) 普通方法 设置cookie的有效域
10 public String getDomain() 普通方法 获取cookie的有效域

  response接口也中定义了一个addCookie方法,它用于在其响应头中增加一个相应的Set-Cookie头字段。 同样,request接口中也定义了一个getCookies方法,它用于获取客户端提交的Cookie。

  • Cookie类用于创建一个Cookie对象
  • response接口中定义了一个addCookie方法,它用于在其响应头中增加一个相应的Set-Cookie头字段
  • request接口中定义了一个getCookies方法,它用于获取客户端提交的Cookie

简单使用Cookie

  • 创建Cookie对象,发送Cookie给浏览器、
1
2
3
4
5
6
7
8
9
10
11
12
13
//设置response的编码
response.setContentType("text/html;charset=UTF-8");

//创建Cookie对象,指定名称和值
Cookie cookie = new Cookie("username", "zhongfucheng");

//向浏览器给一个Cookie
response.addCookie(cookie);

response.getWriter().write("我已经向浏览器发送了一个Cookie");

//设置Cookie的时间
cookie.setMaxAge(1000);
  • 保存中文
1
2
3
4
5
6
7
8
9
10
11
12
response.setContentType("text/html;charset=UTF-8");
PrintWriter printWriter = response.getWriter();

String name = "中国";

//对Unicode字符进行编码
Cookie cookie = new Cookie("country", URLEncoder.encode(name, "UTF-8"));

cookie.setMaxAge(2000);
response.addCookie(cookie);

printWriter.write("我颁发了一个Cookie,值保存的是中文数据");
  • 我们发现Cookie保存在硬盘的中文数据是经过编码的,那么我们在取出Cookie的时候要对中文数据进行解码
1
2
3
4
5
6
7
8
9
Cookie[] cookies = request.getCookies();
for (int i = 0; cookies != null && i < cookies.length; i++) {
String name = cookies[i].getName();

//经过URLEncoding就要URLDecoding
String value = URLDecoder.decode(cookies[i].getValue(), "UTF-8");

printWriter.write(name + "------" + value);
}

Cookie不可跨域名性

Cookie具有不可跨域名性。浏览器判断一个网站是否能操作另一个网站的Cookie的依据是域名。所以一般来说,当我访问baidu的时候,浏览器只会把baidu颁发的Cookie带过去,而不会带上google的Cookie。

Cookie的有效期

Cookie的有效期是通过setMaxAge()来设置的

  • 如果MaxAge为正数浏览器会把Cookie写到硬盘中,只要还在MaxAge秒之前,登陆网站时该Cookie就有效【不论关闭了浏览器还是电脑】
  • 如果MaxAge为负数Cookie是临时性的,仅在本浏览器内有效,关闭浏览器Cookie就失效了,Cookie不会写到硬盘中。Cookie默认值就是-1。这也就为什么在我第一个例子中,如果我没设置Cookie的有效期,在硬盘中就找不到对应的文件。
  • 如果MaxAge为0,则表示删除该Cookie。Cookie机制没有提供删除Cookie对应的方法,把MaxAge设置为0等同于删除Cookie

Cookie的修改和删除

  • 上面我们已经知道了Cookie机制没有提供删除Cookie的方法。其实细心点我们可以发现,Cookie机制也没有提供修改Cookie的方法。那么我们怎么修改Cookie的值呢
  • Cookie存储的方式类似于Map集合
  • Cookie的名称相同,通过response添加到浏览器中,会覆盖原来的Cookie
  • 注意:删除,修改Cookie时,新建的Cookie除了value、maxAge之外的所有属性都要与原Cookie相同。否则浏览器将视为不同的Cookie,不予覆盖,导致删除修改失败

删除Cookie示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CookieDemo02 extends HttpServlet {

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//创建一个名字为lastAccessTime的cookie
Cookie cookie = new Cookie("lastAccessTime", System.currentTimeMillis()+"");
//将cookie的有效期设置为0,命令浏览器删除该cookie
cookie.setMaxAge(0);
response.addCookie(cookie);
}

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

Cookie的域名

Cookie的domain属性决定运行访问Cookie的域名。domain的值规定为“.域名”

Cookie的安全属性

  • HTTP协议不仅仅是无状态的,而且是不安全的!如果不希望Cookie在非安全协议中传输,可以设置Cookie的secure属性为true,浏览器只会在HTTPS和SSL等安全协议中传输该Cookie
  • 当然了,设置secure属性不会将Cookie的内容加密。如果想要保证安全,最好使用md5算法加密。

Session

Session 是另一种记录浏览器状态的机制。不同的是Cookie保存在浏览器中,Session保存在服务器中。用户使用浏览器访问服务器的时候,服务器把用户的信息以某种的形式记录在服务器,这就是Session

Session和Cookie的区别

  • Cookie是把用户的数据写给用户的浏览器。
  • Session技术把用户的数据写到用户独占的session中。
  • Session对象由服务器创建,开发人员可以调用request对象的getSession方法得到session对象。
  • Session可以存储对象,Cookie只能存储字符串。

Session的生命周期和有效期

  • Session在用户第一次访问服务器Servlet,jsp等动态资源就会被自动创建,Session对象保存在内存里
  • 如果访问HTML,IMAGE等静态资源Session不会被创建。
  • Session生成后,只要用户继续访问,服务器就会更新Session的最后访问时间,无论是否对Session进行读写,服务器都会认为Session活跃了一次
  • 由于会有越来越多的用户访问服务器,因此Session也会越来越多。为了防止内存溢出,服务器会把长时间没有活跃的Session从内存中删除,这个时间也就是Session的超时时间

Session创建和销毁

服务器创建session出来后,会把session的id号,以cookie的形式回写给客户机,这样,只要客户机的浏览器不关,再去访问服务器时,都会带着session的id号去,服务器发现客户机浏览器带session id过来了,就会使用内存中与之对应的session为之服务。

创建:

1
2
3
4
5
6
7
8
9
10
//使用request对象的getSession()获取session,如果session不存在则创建一个
HttpSession session = request.getSession();
//获取session的Id
String sessionId = session.getId();
//判断session是不是新创建的
if (session.isNew()) {
response.getWriter().print("session创建成功,session的id是:"+sessionId);
}else {
response.getWriter().print("服务器已经存在session,session的id是:"+sessionId);
}

销毁:

 session对象默认30分钟没有使用,则服务器会自动销毁session,在web.xml文件中可以手工配置session的失效时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<display-name></display-name>

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

<!-- 设置Session的有效时间:以分钟为单位-->
<session-config>
<session-timeout>15</session-timeout>
</session-config>

</web-app>
  • 在tomcat/conf/web.xml文件中设置,时间值为20分钟,所有的WEB应用都有效
  • 在单个的web.xml文件中设置,对单个web应用有效,如果有冲突,以自己的web应用为准

当需要在程序中手动设置Session失效时,可以手工调用session.invalidate方法,摧毁session。

1
2
3
HttpSession session = request.getSession();
//手工调用session.invalidate方法,摧毁session
session.invalidate();

Session应用

1、表单重复提交

  • 网络延迟
  • 表单提交后,用户多次刷新
  • 表单提交后,点击浏览器“后退”按钮回退到表单页面后再次提交

方法一:只能解决在网络延迟的情况下防止用户多次提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DoFormServlet extends HttpServlet {

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//客户端是以UTF-8编码传输数据到服务器端的,所以需要设置服务器端以UTF-8的编码进行接收,否则对于中文数据就会产生乱码
request.setCharacterEncoding("UTF-8");
String userName = request.getParameter("username");
try {
//让当前的线程睡眠3秒钟,模拟网络延迟而导致表单重复提交的现象
Thread.sleep(3*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("向数据库中插入数据:"+userName);
}

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

}

方法二:利用Session

具体的做法:在服务器端生成一个唯一的随机标识号,专业术语称为Token(令牌),同时在当前用户的Session域中保存这个Token。然后将Token发送到客户端的Form表单中,在Form表单中使用隐藏域来存储这个Token,表单提交的时候连同这个Token一起提交到服务器端,然后在服务器端判断客户端提交上来的Token与服务器端生成的Token是否一致,如果不一致,那就是重复提交了,此时服务器端就可以不处理重复提交的表单。如果相同则处理表单提交,处理完后清除当前用户的Session域中存储的标识号。

在下列情况下,服务器程序将拒绝处理用户提交的表单请求:

  1. 存储Session域中的Token(令牌)与表单提交的Token(令牌)不同。
  2. 当前用户的Session中不存在Token(令牌)。
  3. 用户提交的表单数据中没有Token(令牌)。

创建Token:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class FormServlet extends HttpServlet {
private static final long serialVersionUID = -884689940866074733L;

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

String token = TokenProccessor.getInstance().makeToken();//创建令牌
System.out.println("在FormServlet中生成的token:"+token);
request.getSession().setAttribute("token", token); //在服务器使用session保存token(令牌)
request.getRequestDispatcher("/form.jsp").forward(request, response);//跳转到form.jsp页面
}

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

}

在jsp文件中使用隐藏域来存储Token:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>form表单</title>
</head>

<body>
<form action="${pageContext.request.contextPath}/servlet/DoFormServlet" method="post">
<%--使用隐藏域存储生成的token--%>
<%--
<input type="hidden" name="token" value="<%=session.getAttribute("token") %>">
--%>
<%--使用EL表达式取出存储在session中的token--%>
<input type="hidden" name="token" value="${token}"/>
用户名:<input type="text" name="username">
<input type="submit" value="提交">
</form>
</body>
</html>

处理表单请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class DoFormServlet extends HttpServlet {

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

boolean b = isRepeatSubmit(request);//判断用户是否是重复提交
if(b==true){
System.out.println("请不要重复提交");
return;
}
request.getSession().removeAttribute("token");//移除session中的token
System.out.println("处理用户提交请求!!");
}

/**
* 判断客户端提交上来的令牌和服务器端生成的令牌是否一致
* @param request
* @return
* true 用户重复提交了表单
* false 用户没有重复提交表单
*/
private boolean isRepeatSubmit(HttpServletRequest request) {
String client_token = request.getParameter("token");
//1、如果用户提交的表单数据中没有token,则用户是重复提交了表单
if(client_token==null){
return true;
}
//取出存储在Session中的token
String server_token = (String) request.getSession().getAttribute("token");
//2、如果当前用户的Session中不存在Token(令牌),则用户是重复提交了表单
if(server_token==null){
return true;
}
//3、存储在Session中的Token(令牌)与表单提交的Token(令牌)不同,则用户是重复提交了表单
if(!client_token.equals(server_token)){
return true;
}

return false;
}

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

}

HttpServlet抽象类

HttpServlet抽象类是继承于GenericServlet抽象类而来的。使用HttpServlet抽象类时,还需要借助分别代表Servlet请求和Servlet响应的HttpServletRequest和HttpServletResponse对象。

  • HttpServletRequest接口扩展于javax.servlet.ServletRequest接口

  • HttpServletResponse接口扩展于javax.servlet.servletResponse接口。

request和response对象

Tomcat收到客户端的http请求,会针对每一次请求,分别创建一个代表请求的request对象、和代表响应的response对象

request对象代表http请求:获取浏览器提交过来的数据,找request对象

response对象代表http响应:向浏览器输出数据,找response对象

HttpServletResponse对象

HttpServletResponse对象代表服务器的响应。这个对象中封装了向客户端发送数据、发送响应头,发送响应状态码的方法。

常见应用

1、使用OutputStream流向客户端浏览器输出中文数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class ResponseDemo01 extends HttpServlet {

private static final long serialVersionUID = 4312868947607181532L;

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
outputChineseByOutputStream(response);//使用OutputStream流输出中文
}

/**
* 使用OutputStream流输出中文
* @param request
* @param response
* @throws IOException
*/
public void outputChineseByOutputStream(HttpServletResponse response) throws IOException{
/**使用OutputStream输出中文注意问题:
* 在服务器端,数据是以哪个码表输出的,那么就要控制客户端浏览器以相应的码表打开,
* 比如:outputStream.write("中国".getBytes("UTF-8"));//使用OutputStream流向客户端浏览器输出中文,以UTF-8的编码进行输出
* 此时就要控制客户端浏览器以UTF-8的编码打开,否则显示的时候就会出现中文乱码,那么在服务器端如何控制客户端浏览器以以UTF-8的编码显示数据呢?
* 可以通过设置响应头控制浏览器的行为,例如:
* response.setHeader("content-type", "text/html;charset=UTF-8");//通过设置响应头控制浏览器以UTF-8的编码显示数据
*/
String data = "中国";
OutputStream outputStream = response.getOutputStream();//获取OutputStream输出流
response.setHeader("content-type", "text/html;charset=UTF-8");//通过设置响应头控制浏览器以UTF-8的编码显示数据,如果不加这句话,那么浏览器显示的将是乱码
/**
* data.getBytes()是一个将字符转换成字节数组的过程,这个过程中一定会去查码表,
* 如果是中文的操作系统环境,默认就是查找查GB2312的码表,
* 将字符转换成字节数组的过程就是将中文字符转换成GB2312的码表上对应的数字
* 比如: "中"在GB2312的码表上对应的数字是98
* "国"在GB2312的码表上对应的数字是99
*/
/**
* getBytes()方法如果不带参数,那么就会根据操作系统的语言环境来选择转换码表,如果是中文操作系统,那么就使用GB2312的码表
*/
byte[] dataByteArr = data.getBytes("UTF-8");//将字符转换成字节数组,指定以UTF-8编码进行转换
outputStream.write(dataByteArr);//使用OutputStream流向客户端输出字节数组
}

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

}

2、使用PrintWriter流向客户端浏览器输出中文数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class ResponseDemo01 extends HttpServlet {

private static final long serialVersionUID = 4312868947607181532L;

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
outputChineseByPrintWriter(response);//使用PrintWriter流输出中文
}

/**
* 使用PrintWriter流输出中文
* @param request
* @param response
* @throws IOException
*/
public void outputChineseByPrintWriter(HttpServletResponse response) throws IOException{
String data = "中国";

//通过设置响应头控制浏览器以UTF-8的编码显示数据,如果不加这句话,那么浏览器显示的将是乱码
//response.setHeader("content-type", "text/html;charset=UTF-8");

response.setCharacterEncoding("UTF-8");//设置将字符以"UTF-8"编码输出到客户端浏览器
/**
* PrintWriter out = response.getWriter();这句代码必须放在response.setCharacterEncoding("UTF-8");之后
* 否则response.setCharacterEncoding("UTF-8")这行代码的设置将无效,浏览器显示的时候还是乱码
*/
PrintWriter out = response.getWriter();//获取PrintWriter输出流
/**
* 多学一招:使用HTML语言里面的<meta>标签来控制浏览器行为,模拟通过设置响应头控制浏览器行为
* out.write("<meta http-equiv='content-type' content='text/html;charset=UTF-8'/>");
* 等同于response.setHeader("content-type", "text/html;charset=UTF-8");
*/
out.write("<meta http-equiv='content-type' content='text/html;charset=UTF-8'/>");
out.write(data);//使用PrintWriter流向客户端输出字符
}

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

3、使用OutputStream或者PrintWriter向客户端浏览器输出数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ResponseDemo01 extends HttpServlet {

private static final long serialVersionUID = 4312868947607181532L;

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

outputOneByOutputStream(response);//使用OutputStream输出1到客户端浏览器

}

/**
* 使用OutputStream流输出数字1
* @param request
* @param response
* @throws IOException
*/
public void outputOneByOutputStream(HttpServletResponse response) throws IOException{
response.setHeader("content-type", "text/html;charset=UTF-8");
OutputStream outputStream = response.getOutputStream();
outputStream.write("使用OutputStream流输出数字1:".getBytes("UTF-8"));
//1+"":数字1就变成了字符串1
outputStream.write((1+"").getBytes());
}

}

4、文件下载

文件下载功能的实现思路:

  1.获取要下载的文件的绝对路径

  2.获取要下载的文件名

  3.设置content-disposition响应头控制浏览器以下载的形式打开文件

  4.获取要下载的文件输入流

  5.创建数据缓冲区

  6.通过response对象获取OutputStream流

  7.将FileInputStream流写入到buffer缓冲区

  8.使用OutputStream将缓冲区的数据输出到客户端浏览器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* @author gacl
* 文件下载
*/
public class ResponseDemo02 extends HttpServlet {

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
downloadFileByOutputStream(response);//下载文件,通过OutputStream流
}

/**
* 下载文件,通过OutputStream流
* @param response
* @throws FileNotFoundException
* @throws IOException
*/
private void downloadFileByOutputStream(HttpServletResponse response)
throws FileNotFoundException, IOException {
//1.获取要下载的文件的绝对路径
String realPath = this.getServletContext().getRealPath("/download/1.JPG");
//2.获取要下载的文件名
String fileName = realPath.substring(realPath.lastIndexOf("\\")+1);
//3.设置content-disposition响应头控制浏览器以下载的形式打开文件
response.setHeader("content-disposition", "attachment;filename="+fileName);
//4.获取要下载的文件输入流
InputStream in = new FileInputStream(realPath);
int len = 0;
//5.创建数据缓冲区
byte[] buffer = new byte[1024];
//6.通过response对象获取OutputStream流
OutputStream out = response.getOutputStream();
//7.将FileInputStream流写入到buffer缓冲区
while ((len = in.read(buffer)) > 0) {
//8.使用OutputStream将缓冲区的数据输出到客户端浏览器
out.write(buffer,0,len);
}
in.close();
}

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

当下载中文文件时,文件名需要经过URL编码,否则会出现文件名乱码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* @author gacl
* 文件下载
*/
public class ResponseDemo02 extends HttpServlet {

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
downloadChineseFileByOutputStream(response);//下载中文文件
}

/**
* 下载中文文件,中文文件下载时,文件名要经过URL编码,否则会出现文件名乱码
* @param response
* @throws FileNotFoundException
* @throws IOException
*/
private void downloadChineseFileByOutputStream(HttpServletResponse response)
throws FileNotFoundException, IOException {
String realPath = this.getServletContext().getRealPath("/download/张家界国家森林公园.JPG");//获取要下载的文件的绝对路径
String fileName = realPath.substring(realPath.lastIndexOf("\\")+1);//获取要下载的文件名
//设置content-disposition响应头控制浏览器以下载的形式打开文件,中文文件名要使用URLEncoder.encode方法进行编码,否则会出现文件名乱码
response.setHeader("content-disposition", "attachment;filename="+URLEncoder.encode(fileName, "UTF-8"));
InputStream in = new FileInputStream(realPath);//获取文件输入流
int len = 0;
byte[] buffer = new byte[1024];
OutputStream out = response.getOutputStream();
while ((len = in.read(buffer)) > 0) {
out.write(buffer,0,len);//将缓冲区的数据输出到客户端浏览器
}
in.close();
}

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

文件下载注意事项:编写文件下载功能时推荐使用OutputStream流,避免使用PrintWriter流,因为OutputStream流是字节流,可以处理任意类型的数据,而PrintWriter流是字符流,只能处理字符数据,如果用字符流处理字节数据,会导致数据丢失。

5、生成验证码

生成图片需要使用到bufferedImage类

javaweb学习总结(九)—— 通过Servlet生成验证码图片 - 孤傲苍狼 - 博客园 (cnblogs.com)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class ResponseDemo03 extends HttpServlet {

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

response.setHeader("refresh", "5");//设置refresh响应头控制浏览器每隔5秒钟刷新一次
//1.在内存中创建一张图片
BufferedImage image = new BufferedImage(80, 20, BufferedImage.TYPE_INT_RGB);
//2.得到图片
//Graphics g = image.getGraphics();
Graphics2D g = (Graphics2D)image.getGraphics();
g.setColor(Color.WHITE);//设置图片的背景色
g.fillRect(0, 0, 80, 20);//填充背景色
//3.向图片上写数据
g.setColor(Color.BLUE);//设置图片上字体的颜色
g.setFont(new Font(null, Font.BOLD, 20));
g.drawString(makeNum(), 0, 20);
//4.设置响应头控制浏览器浏览器以图片的方式打开
response.setContentType("image/jpeg");//等同于response.setHeader("Content-Type", "image/jpeg");
//5.设置响应头控制浏览器不缓存图片数据
response.setDateHeader("expries", -1);
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "no-cache");
//6.将图片写给浏览器
ImageIO.write(image, "jpg", response.getOutputStream());
}

/**
* 生成随机数字
* @return
*/
private String makeNum(){
Random random = new Random();
String num = random.nextInt(9999999)+"";
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 7-num.length(); i++) {
sb.append("0");
}
num = sb.toString()+num;
return num;
}

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

}

6、设置响应头控制浏览器行为

  • 设置http响应头控制浏览器禁止缓存当前文档内容
1
2
3
response.setDateHeader("expries", -1);
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "no-cache");
  • 设置http响应头控制浏览器定时刷新网页(refresh)

    1
    response.setHeader("refresh", "5");//设置refresh响应头控制浏览器每隔5秒钟刷新一次

7、通过response实现请求重定向

请求重定向指:一个web资源收到客户端请求后,通知客户端去访问另外一个web资源,这称之为请求重定向。

  • 应用场景:用户登陆,用户首先访问登录页面,登录成功后,就会跳转到某个页面,这个过程就是一个请求重定向的过程

  • 实现方式:response.sendRedirect(String location),即调用response对象的sendRedirect方法实现请求重定向  

  • sendRedirect内部的实现原理:使用response设置302状态码和设置location响应头实现重定向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ResponseDemo04 extends HttpServlet {

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
/**
* 1.调用sendRedirect方法实现请求重定向,
* sendRedirect方法内部调用了
* response.setHeader("Location", "/JavaWeb_HttpServletResponse_Study_20140615/index.jsp");
* response.setStatus(HttpServletResponse.SC_FOUND);//设置302状态码,等同于response.setStatus(302);
*/
response.sendRedirect("/JavaWeb_HttpServletResponse_Study_20140615/index.jsp");
/*其实sendRedirect()方法就是对setStatus()和setHeader()进行封装,原理就是setStatus()和setHeader()*/


//2.使用response设置302状态码和设置location响应头实现重定向实现请求重定向
//response.setHeader("Location", "/JavaWeb_HttpServletResponse_Study_20140615/index.jsp");
//response.setStatus(HttpServletResponse.SC_FOUND);//设置302状态码,等同于response.setStatus(302);
}

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

8、数据压缩

GZIP压缩类可对数据进行压缩:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
		//GZIP的构造方法需要一个OutputStream子类对象,究竟哪个对象适合,我们看下write()方法
GZIPOutputStream gzipOutputStream = new GZIPOutputStream();

//查看了下API,write()接收的是byte[]类型的。
gzipOutputStream.write();


//既然是byte[]类型,那么我就给他一个ByteArrayOutputStream
//不能以匿名内部类的方式给GZIPOutputStream,必须把ByteArrayOutputStream定义出来
//因为GZIPOutputStream写数据的时候,是把数据写到ByteArrayOutputStream上的,等会还要把数据取出来,再写给浏览器
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(new ByteArrayOutputStream());

//创建GZIPOutputStream对象,给予它ByteArrayOutputStream
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream);

//GZIP对数据压缩,GZIP写入的数据是保存在byteArrayOutputStream上的
gzipOutputStream.write(ss.getBytes());

//gzipOutputStream有缓冲,把缓冲清了,并顺便关闭流
gzipOutputStream.close();

//将压缩的数据取出来
byte[] bytes = byteArrayOutputStream.toByteArray();

//将压缩的数据写给浏览器
response.getOutputStream().write(bytes);

//压缩后的数据如果不告诉浏览器是压缩数据会乱码
//告诉浏览器这是gzip压缩的数据
response.setHeader("Content-Encoding","gzip");

//再将压缩的数据写给浏览器
response.getOutputStream().write(bytes);

输出流——getWriter()和getOutputStream()

  1. getWriter()和getOutputStream()两个方法不能同时调用
  2. Servlet程序向ServletOutputStream或PrintWriter对象中写入的数据将被Servlet引擎从response里面获取,Servlet引擎将这些数据当作响应消息的正文,然后再与响应状态行和各响应头组合后输出到客户端
  3. Servlet的serice()方法结束后【也就是doPost()或者doGet()结束后】,Servlet引擎将检查getWriter或getOutputStream方法返回的输出流对象是否已经调用过close方法,如果没有,Servlet引擎将调用close方法关闭该输出流对象.

HttpServletRequest对象

HttpServletRequest对象代表客户端的请求,当客户端通过HTTP协议访问服务器时,HTTP请求头中的所有信息都封装在这个对象中,开发人员通过这个对象的方法,可以获得客户这些信息。

简单来说,要得到浏览器信息,就找HttpServletRequest对象

request对象作为一个域对象(Map容器)使用时,主要是通过以下的四个方法来操作

  • setAttribute(String name,Object o)方法,将数据作为request对象的一个属性存放到request对象中,例如:request.setAttribute(“data”, data);
  • getAttribute(String name)方法,获取request对象的name属性的属性值,例如:request.getAttribute(“data”)
  • removeAttribute(String name)方法,移除request对象的name属性,例如:request.removeAttribute(“data”)
  • getAttributeNames方法,获取request对象的所有属性名,返回的是一个,例如:Enumeration attrNames = request.getAttributeNames();

常见api

获得客户机【浏览器】信息

  • getRequestURL方法返回客户端发出请求时的完整URL。
  • getRequestURI方法返回请求行中的资源名部分。
  • getQueryString 方法返回请求行中的参数部分。
  • getPathInfo方法返回请求URL中的额外路径信息。额外路径信息是请求URL中的位于Servlet的路径之后和查询参数之前的内容,它以“/”开头。
  • getRemoteAddr方法返回发出请求的客户机的IP地址
  • getRemoteHost方法返回发出请求的客户机的完整主机名
  • getRemotePort方法返回客户机所使用的网络端口号
  • getLocalAddr方法返回WEB服务器的IP地址。
  • getLocalName方法返回WEB服务器的主机名

获得客户机请求头

  • getHeader(string name)方法:String  
  • getHeaders(String name)方法:Enumeration
  • getHeaderNames()方法

获得客户机请求参数(客户端提交的数据)

  • getParameter(String)方法**(常用)**
  • getParameterValues(String name)方法**(常用)**
  • getParameterNames()方法(不常用)
  • getParameterMap()方法**(编写框架时常用)**

具体用法:javaweb学习总结(十)——HttpServletRequest对象(一) - 孤傲苍狼 - 博客园 (cnblogs.com)

常见应用

1、中文参数乱码问题

产生乱码,就是因为服务器和客户端沟通的编码不一致造成的,因此解决的办法是:在客户端和服务器之间设置一个统一的编码,之后就按照此编码进行数据的传输和接收。

由于客户端是以UTF-8字符编码将表单数据传输到服务器端的,因此服务器也需要设置以UTF-8字符编码进行接收,要想完成此操作,服务器可以直接使用从ServletRequest接口继承而来的”setCharacterEncoding(charset)”方法进行统一的编码设置。修改后的代码如下:

1
2
3
4
5
6
7
8
9
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
/**
* 客户端是以UTF-8编码传输数据到服务器端的,所以需要设置服务器端以UTF-8的编码进行接收,否则对于中文数据就会产生乱码
*/
request.setCharacterEncoding("UTF-8");
String userName = request.getParameter("userName");
System.out.println("userName:"+userName);
}

以get方式提交表单中文参数乱码问题

单纯使用 request.setCharacterEncoding(“UTF-8”); 无法解决问题。

解决办法:在接收到数据后,先获取request对象以ISO8859-1字符编码接收到的原始数据的字节数组,然后通过字节数组以指定的编码构建字符串,解决乱码问题。

1
2
3
4
5
6
7
8
9
10
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
/**
*
* 对于以get方式传输的数据,request即使设置了以指定的编码接收数据也是无效的,默认的还是使用ISO8859-1这个字符编码来接收数据
*/
String name = request.getParameter("name");//接收数据
name =new String(name.getBytes("ISO8859-1"), "UTF-8") ;//获取request对象以ISO8859-1字符编码接收到的原始数据的字节数组,然后通过字节数组以指定的编码构建字符串,解决乱码问题
System.out.println("name:"+name);
}

2、防盗链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//获取到网页是从哪里来的
String referer = request.getHeader("Referer");

//如果不是从我的首页来或者从地址栏直接访问的,
if ( referer == null || !referer.contains("localhost:8080/zhongfucheng/index.jsp") ) {

//回到首页去
response.sendRedirect("/zhongfucheng/index.jsp");
return;
}

//能执行下面的语句,说明是从我的首页点击进来的,那没问题,照常显示
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("路飞做了XXXXxxxxxxxxxxxxxxxx");

3、实现转发

请求转发:指一个web资源收到客户端请求后,通知服务器去调用另外一个web资源进行处理。
  请求转发的应用场景:MVC设计模式

endRedirect()可以实现重定向,做到的功能是页面跳转,使用request的getRequestDispatcher.forward(request,response)实现转发,做到的功能也是页面跳转

1
2
3
4
5
//获取到requestDispatcher对象,跳转到index.jsp
RequestDispatcher requestDispatcher = request.getRequestDispatcher("/index.jsp");

//调用requestDispatcher对象的forward()实现转发,传入request和response方法
requestDispatcher.forward(request, response);
  • ServletContext也能称之为域对象。而request也可以称之为域对象,只不过ServletContext的域是整个web应用,而request的域仅仅代表一次http请求
  • 一般的原则:可以使用request就尽可能使用request。因为ServletContext代表着整个web应用,使用ServletContext会消耗大量的资源,而request对象会随着请求的结束而结束,资源会被回收使用request域进行Servlet之间的通讯在开发中是非常频繁的

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class RequestDemo06 extends HttpServlet {

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

String data="大家好";
/**
* 将数据存放到request对象中,此时把request对象当作一个Map容器来使用
*/
request.setAttribute("data", data);
//客户端访问RequestDemo06这个Servlet后,RequestDemo06通知服务器将请求转发(forward)到test.jsp页面进行处理
request.getRequestDispatcher("/test.jsp").forward(request, response);
}

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

test.jsp页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>Request对象实现请求转发</title>
</head>

<body>
使用普通方式取出存储在request对象中的数据:
<h3 style="color:red;"><%=(String)request.getAttribute("data")%></h3>
使用EL表达式取出存储在request对象中的数据:
<h3 style="color:red;">${data}</h3>
</body>
</html>

转发和重定向的区别

实际发生位置不同,地址栏不同

  • 转发是发生在服务器的
    • 转发是由服务器进行跳转的,细心的朋友会发现,在转发的时候,浏览器的地址栏是没有发生变化的。也就是说浏览器是不知道该跳转的动作,转发是对浏览器透明的。通过上面的转发时序图我们也可以发现,实现转发只是一次的http请求一次转发中request和response对象都是同一个。这也解释了,为什么可以使用request作为域对象进行Servlet之间的通讯。
  • 重定向是发生在浏览器的
    • 重定向是由浏览器进行跳转的,进行重定向跳转的时候,浏览器的地址会发生变化的。曾经介绍过:实现重定向的原理是由response的状态码和Location头组合而实现的。这是由浏览器进行的页面跳转实现重定向会发出两个http请求request域对象是无效的,因为它不是同一个request对象

用法不同

很多人都搞不清楚转发和重定向的时候,资源地址究竟怎么写。有的时候要把应用名写上,有的时候不用把应用名写上。很容易把人搞晕。记住一个原则:给服务器用的直接从资源名开始写,给浏览器用的要把应用名写上

  • request.getRequestDispatcher(“/资源名 URI”).forward(request,response)
    • 转发时”/“代表的是本应用程序的根目录
  • response.send(“/web应用/资源名 URI”);
    • 重定向时”/“代表的是webapps目录

能够去往的URL的范围不一样

  • 转发是服务器跳转只能去往当前web应用的资源
  • 重定向是浏览器跳转,可以去往任何的资源

传递数据的类型不同

  • 转发的request对象可以传递各种类型的数据,包括对象
  • 重定向只能传递字符串

跳转的时间不同

  • 转发时:执行到跳转语句时就会立刻跳转
  • 重定向:整个页面执行完之后才执行跳转

转发和重定向使用哪一个?

根据上面说明了转发和重定向的区别也可以很容易概括出来。转发是带着转发前的请求的参数的。重定向是新的请求

典型的应用场景:

  1. 转发: 访问 Servlet 处理业务逻辑,然后 forward 到 jsp 显示处理结果,浏览器里 URL 不变
  2. 重定向: 提交表单,处理成功后 redirect 到另一个 jsp,防止表单重复提交,浏览器里 URL 变了

Servlet

是用Java编写的服务器端程序。其主要功能在于交互式地浏览和修改数据,生成动态Web内容。

狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的类。

Servlet运行于支持Java的应用服务器中。从实现上讲,Servlet可以响应任何类型的请求,但绝大多数情况下Servlet只用来扩展基于HTTP协议的Web服务器。

Servlet带给我们最大的作用就是能够处理浏览器带来HTTP请求,并返回一个响应给浏览器,从而实现浏览器和服务器的交互

Servlet的工作模式

  • 客户端发送请求至web服务器
  • web服务器收到客户端的Servlet访问请求后:
    1. web服务器检查是否已经装载并创建了该Servlet的实例对象。如果是,则直接执行第④步,否则,执行第②步。
    2. 装载并创建该Servlet的一个实例对象。
    3. 调用Servlet实例对象的init()方法。
    4. 创建一个用于封装HTTP请求消息的HttpServletRequest对象和一个代表HTTP响应消息的HttpServletResponse对象,然后调用Servlet的service()方法并将请求和响应对象作为参数传递进去。
    5. WEB应用程序被停止或重新启动之前,Servlet引擎将卸载Servlet,并在卸载之前调用Servlet的destroy()方法。
  • 服务器将响应返回客户端

Servlet的生命周期

  1. 加载Servlet。当Tomcat第一次访问Servlet的时候,Tomcat会负责创建Servlet的实例
  2. 初始化。当Servlet被实例化后,Tomcat会调用init()方法初始化这个对象
  3. 处理服务。当浏览器访问Servlet的时候,Servlet 会调用service()方法处理请求
  4. 销毁。当Tomcat关闭时或者检测到Servlet要从Tomcat删除的时候会自动调用destroy()方法,让该实例释放掉所占的资源。一个Servlet如果长时间不被使用的话,也会被Tomcat自动销毁
  5. 卸载。当Servlet调用完destroy()方法后,等待垃圾回收。如果有需要再次使用这个Servlet,会重新调用init()方法进行初始化操作
  • 简单总结:只要访问Servlet,service()就会被调用。init()只有第一次访问Servlet的时候才会被调用。destroy()只有在Tomcat关闭的时候才会被调用。

Servlet的特点

映射关系

  • 一个已经注册的Servlet可以被多次映射同一个Servlet可以被映射到多个URL上。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    <!--
    当浏览器访问http://localhost:8080/Demo1时:
    1、发现Tomcat中的web.xml有配置这个映射路径 <url-pattern>/Demo1</url-pattern>
    2、查看映射路径的名字 <servlet-name>Demo1</servlet-name>
    3、通过映射路径的名字找到配置servlet的名字 <servlet-name>Demo1</servlet-name>
    4、通过servlet名找到servlet编译后class文件存放的位置
    5、执行MyServlet <servlet-class>
    -->
    <servlet>
    <!--为Demo1配一个名字,一般都会和类名一致-->
    <servlet-name>Demo1</servlet-name>
    <!--类的存放位置在哪里【有包名的话需要添加上包名】-->
    <servlet-class>zhongfucheng.web.Demo1</servlet-class>
    </servlet>
    <!--为Demo1配置映射路径-->
    <servlet-mapping>
    <servlet-name>Demo1</servlet-name>
    <!--外界访问Demo1的路径-->
    <url-pattern>/Demo1</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
    <servlet-name>Demo1</servlet-name>
    <url-pattern>/ouzicheng</url-pattern>
    </servlet-mapping>


    无论我访问的是http://localhost:8080/Demo1还是http://localhost:8080/ouzicheng。我访问的都是Demo1。
  • Servlet映射的URL可以使用通配符

  • 在servlet3.0以后,web.xml中对Servlet配置,同样可以在@WebServlet注解中配置.
    下面是@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> 标签。
    description String 该 Servlet 的描述信息,等价于 <description>标签。
    displayName String 该 Servlet 的显示名,通常配合工具使用,等价于 <display-name>标签。

Servlet————单例模式

  • 浏览器多次对Servlet的请求,一般情况下,服务器只创建一个Servlet对象,也就是说,Servlet对象一旦创建了,就会驻留在内存中,为后续的请求做服务,直到服务器关闭
  • 在Servlet的整个生命周期内,Servlet的init方法只被调用一次。而对一个Servlet的每次访问请求都导致Servlet引擎调用一次servlet的service方法。
  • 对于每次访问请求,Servlet引擎都会创建一个新的HttpServletRequest请求对象和一个新的HttpServletResponse响应对象,然后将这两个对象作为参数传递给它调用的Servlet的service()方法service方法再根据请求方式分别调用doXXX方法

通配符

  • 通配符有两种格式:

    1. *.扩展名
    2. 正斜杠(/)开头并以“/*”结尾。
    3. *.扩展名的优先级最低

线程安全问题

当多个用户访问Servlet的时候,服务器会为每个用户创建一个线程当多个用户并发访问Servlet共享资源的时候就会出现线程安全问题

原则:

  1. 如果一个变量需要多个用户共享,则应当在访问该变量的时候,加同步机制synchronized (对象){}
  2. 如果一个变量不需要共享,则直接在 doGet() 或者 doPost()定义.这样不会存在线程安全问题

web资源的访问都需要访问Servlet

缺省Servlet用于处理所有其他Servlet都不处理的访问请求。

web站点信息配置

  • web.xml文件支持对整个站点进行配置参数信息所有Servlet都可以取到该参数信息
1
2
3
4
<context-param>
<param-name>name</param-name>
<param-value>XXX</param-value>
</context-param>

web工程中URL地址的写法

如果”/“是给服务器用的,则代表当前的web工程,如果”/“是给浏览器用的,则代表webapps目录。

ServletConfig对象

此对象可以读取web.xml中配置的初始化参数。

————当servlet配置了初始化参数后,web容器在创建servlet实例对象时,会自动将这些初始化参数封装到ServletConfig对象中,并在调用servlet的init方法时,将ServletConfig对象传递给servlet。

参数信息放置在web.xml中可以方便修改。

ServletContext对象

当Tomcat启动的时候,就会创建一个ServletContext对象。它代表着当前web站点

  • 所有Servlet都共享着一个ServletContext对象,所以Servlet之间可以通过ServletContext实现通讯
  • ServletConfig获取的是配置的是单个Servlet的参数信息,ServletContext可以获取的是配置整个web站点的参数信息
  • 利用ServletContext读取web站点的资源文件
  • 实现Servlet的转发【用ServletContext转发不多,主要用request转发】

应用

多个Servlet通过ServletContext对象实现数据共享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//ServletContextDemo1:
public class ServletContextDemo1 extends HttpServlet {

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String data = "xdp_gacl";
/**
* ServletConfig对象中维护了ServletContext对象的引用,开发人员在编写servlet时,
* 可以通过ServletConfig.getServletContext方法获得ServletContext对象。
*/
ServletContext context = this.getServletConfig().getServletContext();//获得ServletContext对象
context.setAttribute("data", data); //将data存储到ServletContext对象中
}

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

//ServletContextDemo2:
public class ServletContextDemo2 extends HttpServlet {

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
ServletContext context = this.getServletContext();
String data = (String) context.getAttribute("data");//从ServletContext对象中取出数据
response.getWriter().print("data="+data);
}

public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
//实现的功能:先运行ServletContextDemo1,将数据data存储到ServletContext对象中,然后运行ServletContextDemo2就可以从ServletContext对象中取出数据了,这样就实现了数据共享

获取WEB应用的初始化参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<display-name></display-name>
<!-- 配置WEB应用的初始化参数 -->
<context-param>
<param-name>url</param-name>
<param-value>jdbc:mysql://localhost:3306/test</param-value>
</context-param>

<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ServletContextDemo3 extends HttpServlet {

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

ServletContext context = this.getServletContext();
//获取整个web站点的初始化参数
String contextInitParam = context.getInitParameter("url");
response.getWriter().print(contextInitParam);
}

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

}

用servletContext实现请求转发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//ServletContextDemo4:
public class ServletContextDemo4 extends HttpServlet {

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String data = "<h1><font color='red'>abcdefghjkl</font></h1>";
response.getOutputStream().write(data.getBytes());
ServletContext context = this.getServletContext();//获取ServletContext对象
RequestDispatcher rd = context.getRequestDispatcher("/servlet/ServletContextDemo5");//获取请求转发对象(RequestDispatcher)
rd.forward(request, response);//调用forward方法实现请求转发
}

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

//ServletContextDemo5:
public class ServletContextDemo5 extends HttpServlet {

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.getOutputStream().write("servletDemo5".getBytes());
}

public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
this.doGet(request, response);
}
}
//浏览器访问ServletContextDemo4,浏览显示的却是ServletContextDemo5的内容

利用ServletContext对象读取资源文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/**
* 使用servletContext读取资源文件
*
* @author gacl
*
*/
public class ServletContextDemo6 extends HttpServlet {

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
/**
* response.setContentType("text/html;charset=UTF-8");目的是控制浏览器用UTF-8进行解码;
* 这样就不会出现中文乱码了
*/
response.setHeader("content-type","text/html;charset=UTF-8");
readSrcDirPropCfgFile(response);//读取src目录下的properties配置文件
response.getWriter().println("<hr/>");
readWebRootDirPropCfgFile(response);//读取WebRoot目录下的properties配置文件
response.getWriter().println("<hr/>");
readPropCfgFile(response);//读取src目录下的db.config包中的db3.properties配置文件
response.getWriter().println("<hr/>");
readPropCfgFile2(response);//读取src目录下的gacl.servlet.study包中的db4.properties配置文件

}

/**
* 读取src目录下的gacl.servlet.study包中的db4.properties配置文件
* @param response
* @throws IOException
*/
private void readPropCfgFile2(HttpServletResponse response)
throws IOException {
InputStream in = this.getServletContext().getResourceAsStream("/WEB-INF/classes/gacl/servlet/study/db4.properties");
Properties prop = new Properties();
prop.load(in);
String driver = prop.getProperty("driver");
String url = prop.getProperty("url");
String username = prop.getProperty("username");
String password = prop.getProperty("password");
response.getWriter().println("读取src目录下的gacl.servlet.study包中的db4.properties配置文件:");
response.getWriter().println(
MessageFormat.format(
"driver={0},url={1},username={2},password={3}",
driver,url, username, password));
}

/**
* 读取src目录下的db.config包中的db3.properties配置文件
* @param response
* @throws FileNotFoundException
* @throws IOException
*/
private void readPropCfgFile(HttpServletResponse response)
throws FileNotFoundException, IOException {
//通过ServletContext获取web资源的绝对路径
String path = this.getServletContext().getRealPath("/WEB-INF/classes/db/config/db3.properties");
InputStream in = new FileInputStream(path);
Properties prop = new Properties();
prop.load(in);
String driver = prop.getProperty("driver");
String url = prop.getProperty("url");
String username = prop.getProperty("username");
String password = prop.getProperty("password");
response.getWriter().println("读取src目录下的db.config包中的db3.properties配置文件:");
response.getWriter().println(
MessageFormat.format(
"driver={0},url={1},username={2},password={3}",
driver,url, username, password));
}

/**
* 通过ServletContext对象读取WebRoot目录下的properties配置文件
* @param response
* @throws IOException
*/
private void readWebRootDirPropCfgFile(HttpServletResponse response)
throws IOException {
/**
* 通过ServletContext对象读取WebRoot目录下的properties配置文件
* “/”代表的是项目根目录
*/
InputStream in = this.getServletContext().getResourceAsStream("/db2.properties");
Properties prop = new Properties();
prop.load(in);
String driver = prop.getProperty("driver");
String url = prop.getProperty("url");
String username = prop.getProperty("username");
String password = prop.getProperty("password");
response.getWriter().println("读取WebRoot目录下的db2.properties配置文件:");
response.getWriter().print(
MessageFormat.format(
"driver={0},url={1},username={2},password={3}",
driver,url, username, password));
}

/**
* 通过ServletContext对象读取src目录下的properties配置文件
* @param response
* @throws IOException
*/
private void readSrcDirPropCfgFile(HttpServletResponse response) throws IOException {
/**
* 通过ServletContext对象读取src目录下的db1.properties配置文件
*/
InputStream in = this.getServletContext().getResourceAsStream("/WEB-INF/classes/db1.properties");
Properties prop = new Properties();
prop.load(in);
String driver = prop.getProperty("driver");
String url = prop.getProperty("url");
String username = prop.getProperty("username");
String password = prop.getProperty("password");
response.getWriter().println("读取src目录下的db1.properties配置文件:");
response.getWriter().println(
MessageFormat.format(
"driver={0},url={1},username={2},password={3}",
driver,url, username, password));
}

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

}

在客户端缓存Servlet的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ServletDemo5 extends HttpServlet {

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String data = "abcddfwerwesfasfsadf";
/**
* 设置数据合理的缓存时间值,以避免浏览器频繁向服务器发送请求,提升服务器的性能
* 这里是将数据的缓存时间设置为1天
*/
response.setDateHeader("expires",System.currentTimeMillis() + 24 * 3600 * 1000);
response.getOutputStream().write(data.getBytes());
}

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

this.doGet(request, response);
}

}

HTTP

常用响应头

  HTTP响应中的常用响应头(消息头)
  Location: 服务器通过这个头,来告诉浏览器跳到哪里
  Server:服务器通过这个头,告诉浏览器服务器的型号
  Content-Encoding:服务器通过这个头,告诉浏览器,数据的压缩格式
  Content-Length: 服务器通过这个头,告诉浏览器回送数据的长度
  Content-Language: 服务器通过这个头,告诉浏览器语言环境
  Content-Type:服务器通过这个头,告诉浏览器回送数据的类型
  Refresh:服务器通过这个头,告诉浏览器定时刷新
  Content-Disposition: 服务器通过这个头,告诉浏览器以下载方式打数据
  Transfer-Encoding:服务器通过这个头,告诉浏览器数据是以分块方式回送的
  Expires: -1 控制浏览器不要缓存
  Cache-Control: no-cache
  Pragma: no-cache

常用的状态码简述

  • 2XX

一般是请求成功

200 正常处理

204 成功处理,但服务器没有新数据返回,显示页面不更新

206 对服务器进行范围请求,只返回一部分数据

  • 3XX

一般表示重定向

301 请求的资源已分配了新的URI中,URL地址改变了。【永久重定向】

302 请求的资源临时分配了新的URI中,URL地址没变【临时重定向】

303 与302相同的功能,但明确客户端应该采用GET方式来获取资源

304 发送了附带请求,但不符合条件【返回未过期的缓存数据】

307 与302相同,但不会把POST请求变成GET

  • 4XX

表示客户端出错了。

400 请求报文语法错误了

401 需要认证身份

403 没有权限访问

404 服务器没有这个资源

  • 5XX

服务器出错了

500 内部资源出错了

503 服务器正忙

HTTP版本区别

HTTP1.0和HTTP1.1最主要的区别就是:

  • HTTP1.1默认是持久化连接

相对于持久化连接还有另外比较重要的改动:

  • HTTP 1.1增加host字段
  • HTTP 1.1中引入了Chunked transfer-coding,范围请求,实现断点续传(实际上就是利用HTTP消息头使用分块传输编码,将实体主体分块传输)
  • HTTP 1.1管线化(pipelining)理论,客户端可以同时发出多个HTTP请求,而不用一个个等待响应之后再请求
    • 注意:这个pipelining仅仅是限于理论场景下,大部分桌面浏览器仍然会选择默认关闭HTTP pipelining!
    • 所以现在使用HTTP1.1协议的应用,都是有可能会开多个TCP连接的!

参考资料:

HTTP2基础

直观比较一下HTTP2和HTTP1.1的区别:

HTTP 1.1提出了管线化(pipelining)理论,但是仅仅是限于理论的阶段上,这个功能默认还是关闭了的。

管线化(pipelining)和非管线化的区别

HTTP Pipelining其实是把多个HTTP请求放到一个TCP连接中一一发送,而在发送过程中不需要等待服务器对前一个请求的响应;只不过,客户端还是要按照发送请求的顺序来接收响应!

HTTP1.1新改动:

  • 持久连接
  • 请求管道化
  • 增加缓存处理(新的字段如cache-control)
  • 增加Host字段、支持断点传输等

HTTP2新改动:

  • 二进制分帧
  • 多路复用
  • 头部压缩
  • 服务器推送

参考资料:

HTTPS

使用了SSL建立安全的通信线路。

HTTPS使用的是共享密钥和公开私有密钥混合来进行加密的。由于公开私有密钥需要太多的资源,不可能一直以公开私有密钥进行通信。因此,HTTP在建立通信线路的时候使用公开私有密钥,当建立完连接后,随后就使用共享密钥进行加密和解密了

  • 对称加密:
    • 加密和解密都是用同一个密钥
  • 非对称加密:
    • 加密用公开的密钥,解密用私钥
    • (私钥只有自己知道,公开的密钥大家都知道)
  • 数字签名:
    • 验证传输的内容是对方发送的数据
    • 发送的数据没有被篡改过
  • 数字证书 (Certificate)
    • 认证机构证明是真实的服务器发送的数据

面试题

HTTP面试题都在这里 - SegmentFault 思否

javaWeb简介

Java web,是用java技术来解决相关web互联网领域的技术的总称。web包括:web服务器和web客户端两部分。

web资源分类:

所谓web资源即放在Internet网上供外界访问的文件或程序,又根据它们呈现的效果及原理不同,将它们划分为静态资源和动态资源。

  • 静态web资源:固定不变数据文件(静态网页 HTML、CSS文件、文本、音频、视频)

  • 静态web技术:HTML+CSS+JavaScript

  • 动态web资源:一段服务程序,运行后,生成的数据文件

  • 动态web技术:servlet,jsp,php, .net ,ruby、python等等

常见的web服务器:

Tomcat:由Apache组织提供的一种Web服务器,提供对jsp和Servlet的支持。它是一种轻量级的javaWeb容器(服务器),也是当前应用最广的JavaWeb服务器(免费)。
Jboss:是一个遵从JavaEE规范的、开放源代码的、纯Java的EJB服务器,它支持所有的JavaEE规范(免费)。
GlassFish: 由Oracle公司开发的一款JavaWeb服务器,是一款强健的商业服务器,达到产品级质量(应用很少,收费)。
Resin:是CAUCHO公司的产品,是一个非常流行的应用服务器,对servlet和JSP提供了良好的支持,性能也比较优良,resin自身采用JAVA语言开发(收费,应用比较多)。
WebLogic:是Oracle公司的产品,是目前应用最广泛的Web服务器,支持JavaEE规范,而且不断的完善以适应新的开发要求,适合大型项目(收费,用的不多,适合大公司)。

JAVAWEB目录结构

以上图说明:

  • bbs目录代表一个web应用
  • bbs目录下的html,jsp文件可以直接被浏览器访问
  • WEB-INF目录下的资源是不能直接被浏览器访问的
  • web.xml文件是web程序的主要配置文件
  • 所有的classes文件都放在classes目录下
  • jar文件放在lib目录下

Tomcat

是一个运行java的网络服务器,在中小型系统和并发量小的场合下被普遍使用,是开发和调试Servlet、JSP 程序的首选。

img

tomcat体系结构

Tomcat中最顶层的容器是Server,代表着整个服务器,从上图中可以看出,一个Server可以包含至少一个Service,即可以包含多个Service,用于具体提供服务。

Service主要包含两个部分:Connector和Container。从上图中可以看出 Tomcat 的心脏就是这两个组件,他们的作用如下:

  • Connector用于处理连接相关的事情,并提供Socket与Request请求和Response响应相关的转化;
  • Container用于封装和管理Servlet,以及具体处理Request请求;

一个Tomcat中只有一个Server,一个Server可以包含多个Service,一个Service只有一个Container,但是可以有多个Connectors,这是因为一个服务可以有多个连接,如同时提供Http和Https链接,也可以提供向相同协议不同端口的连接

多个 Connector 和一个 Container 就形成了一个 Service,有了 Service 就可以对外提供服务了,但是 Service 还要一个生存的环境,必须要有人能够给她生命、掌握其生死大权,那就非 Server 莫属了!所以整个 Tomcat 的生命周期由 Server 控制。

Tomcat 有哪几种Connector 运行模式(优化)?

BIO:同步并阻塞

一个线程处理一个请求。缺点:并发量高时,线程数较多,浪费资源。Tomcat7或以下,在Linux系统中默认使用这种方式。

NIO:同步非阻塞IO

利用Java的异步IO处理,可以通过少量的线程处理大量的请求,可以复用同一个线程处理多个connection(多路复用)。Tomcat8在Linux系统中默认使用这种方式。

APR:即Apache Portable Runtime

从操作系统层面解决io阻塞问题。

Tomcat有几种部署方式?
Tomcat容器是如何创建servlet类实例?用到了什么原理?

  1. 当容器启动时,会读取在webapps目录下所有的web应用中的web.xml文件,然后对 xml文件进行解析,并读取servlet注册信息。然后,将每个应用中注册的servlet类都进行加载,并通过 反射的方式实例化。(有时候也是在第一次请求时实例化)
  2. 在servlet注册时加上1如果为正数,则在一开始就实例化,如果不写或为负数,则第一次请求实例化。

浏览器访问web资源的流程图

有关配置虚拟目录、临时域名、虚拟主机见:参考文献

JavaWeb学习总结(三)——Tomcat服务器学习和使用(二) - 孤傲苍狼 - 博客园 (cnblogs.com)