day14 JDBC

Author:相忠良
Email: ugoood@163.com
起始于:May 29, 2018
最后更新日期:June 6, 2018javascript

声明:本笔记依据传智播客方立勋老师 Java Web 的授课视频内容记录而成,中间加入了本身的理解。本笔记目的是强化本身学习所用。如有疏漏或不当之处,请在评论区指出。谢谢。
涉及的图片,文档写完后,一次性更新。html

day14 JDBC

1. JDBC 入门

JDBC(Java Data Base Connectivity) 是 java 数据库链接,主要有接口组成。
JDBC的做用:能够经过java程序操做数据库。java

1

组成JDBC的2个包:mysql

  • java.sql
  • javax.sql
    这2个包就是JDBC,就是接口。

除导入JDBC外,还需导入相应数据库JDBC接口的实现,即数据库驱动。咱们用的是mysql-connector-java-5.0.8-bin.jar针对mysql的数据库驱动。程序员

作JDBC试验前的准备工做:
user.sql文件,代码以下:web

create database day14 character set utf8 collate utf8_general_ci;

use day14;

create table users(
    id int primary key,
    name varchar(40),
    password varchar(40),
    email varchar(60),
    birthday date
);

insert into users(id,name,password,email,birthday) values(1,'zs','123456','zs@sina.com','1980-12-04');
insert into users(id,name,password,email,birthday) values(2,'lisi','123456','lisi@sina.com','1981-12-04');
insert into users(id,name,password,email,birthday) values(3,'wangwu','123456','wangwu@sina.com','1979-12-04');

而后创建普通java工程day14,并创建lib文件夹,将mysql-connector-java-5.0.8-bin.jar复制到lib,并把该包变为奶瓶。spring

创建Demo1.java,操做 day14 数据库的 users 表,代码以下:sql

package cn.wk.demo;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class Demo1 {
    public static void main(String[] args) throws SQLException {

      /*
      jdbc:mysql 表示协议
      //localchost:3306 本机3306端口,是mysql服务器端口
      day14 要链接的数据库
      */
        String url = "jdbc:mysql://localhost:3306/day14";
        String username = "root";
        String password = "root";

        // 1. 加载驱动
        DriverManager.registerDriver(new com.mysql.jdbc.Driver());

        // 2. 获取链接
        Connection conn = DriverManager.getConnection(url, username, password);

        // 3. 获取向数据库发sql语句的statement对象
        Statement st = conn.createStatement();

        // 4. 向数据库发sql,获取数据库返回的结果集
        ResultSet rs = st.executeQuery("select * from users");

        // 5. 从结果集中获取数据
        while (rs.next()) {
            System.out.print("id = " + rs.getObject("id") + "  ");
            System.out.print("name = " + rs.getObject("name") + "  ");
            System.out.print("password = " + rs.getObject("password") + "  ");
            System.out.print("email = " + rs.getObject("email") + "  ");
            System.out.print("birthday = " + rs.getObject("birthday") + "  ");
            System.out.println();
        }

        // 6. 释放资源(释放连接)
        rs.close();
        st.close();
        conn.close();
    }
}

2. jdbc程序步骤详解1

  1. 加载驱动的细节;
  2. 获取链接的细节。
    以下:

2

3

依据上面所说,修改了上节的代码,代码片断以下:数据库

// localhost简写,?后接各类参数,不管mysql用何种字符集,均推荐字符显式设定
String url = "jdbc:mysql:///day14"
    + "?user=root&password=root"
    + "&useUnicode=true&characterEncoding=UTF-8";

Class.forName("com.mysql.jdbc.Driver"); // <---- 加载类,同时注册
Connection conn = DriverManager.getConnection(url);

3. jdbc程序步骤详解2

Connection对象:
Jdbc程序中的Connection,它用于表明数据库的连接,Collection是数据库编程中最重要的一个对象,客户端与数据库全部交互都是经过connection对象完成的,这个对象的经常使用方法:apache

  • createStatement():建立向数据库发送sql的statement对象。
  • prepareStatement(sql) :建立向数据库发送预编译sql的PrepareSatement对象。
  • prepareCall(sql):建立执行存储过程的callableStatement对象。
  • setAutoCommit(boolean autoCommit):设置事务是否自动提交。
  • commit() :在连接上提交事务。
  • rollback() :在此连接上回滚事务。

Statement对象:
Statement对象用于向数据库发送SQL语句, Statement对象经常使用方法:

  • executeQuery(String sql) :用于向数据发送查询语句。
  • executeUpdate(String sql):用于向数据库发送insert、update或delete语句
  • execute(String sql):用于向数据库发送任意sql语句,通常不用。
  • addBatch(String sql) :把多条sql语句放到一个批处理中。
  • executeBatch():向数据库发送一批sql语句执行。

ResultSet对象:
ResultSet用于表明Sql语句的执行结果。
下图表达了,mysql 表中字段类型与 ResultSet 方法对照表:
4

最后必定要释放 conn,由于mysql的连接资源不多很宝贵,释放技巧以下:
资源释放代码放入finally里。

模板代码 以下:

package cn.wk.demo;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import cn.wk.domain.User;

public class Demo2 {
    public static void main(String[] args) throws SQLException,
            ClassNotFoundException {

        String url = "jdbc:mysql:///day14";
        String username = "root";
        String password = "root";

        Connection conn = null;
        Statement st = null;
        ResultSet rs = null;

        try {
            Class.forName("com.mysql.jdbc.Driver");
            conn = DriverManager.getConnection(url, username, password);
            st = conn.createStatement(); // throw
            rs = st.executeQuery("select * from users");
            while (rs.next()) {
                User user = new User();
                user.setId(rs.getInt("id"));
                user.setName(rs.getString("name"));
                user.setPassword(rs.getString("password"));
                user.setEmail(rs.getString("email"));
                user.setBirthday(rs.getDate("birthday"));
            }
        } finally {
            if (rs != null) {
                try {
                    rs.close(); // throw new
                } catch (Exception e) {
                    e.printStackTrace();
                }
                rs = null;
            }
            if (st != null) {
                try {
                    st.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                st = null;
            }
            if (conn != null) {
                try {
                    conn.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

顺便建了个User类,作为javabean对象,代码以下:

package cn.wk.domain;

import java.util.Date;

public class User {
    private int id;
    private String name;
    private String password;
    private String email;
    private Date birthday;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }
}

4. 使用 jdbc 完成 crud

案例:

  1. Demo3.java实现crud方法;
  2. db.properties资源文件,达到灵活切换数据库驱动、url、username 和 password;
  3. cn.wk.utils.JdbcUtils工具类,实现驱动加载、创建连接和释放连接这种公共代码。

Demo3.java,以下:

package cn.wk.demo;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

import org.junit.Test;

import cn.wk.domain.User;
import cn.wk.utils.JdbcUtils;

public class Demo3 {

    @Test
    public void insert() throws SQLException {
        Connection conn = null;
        Statement st = null;
        ResultSet rs = null;
        try {
            conn = JdbcUtils.getConnection();
            st = conn.createStatement();
            String sql = "insert into users(id,name,password,email,birthday)"
                    + "values (4,'eee','123','ee@sina.com','1980-11-11')";
            int num = st.executeUpdate(sql);
            if (num > 0) {
                System.out.println("插入成功!!!");
            }
        } finally {
            JdbcUtils.release(conn, st, rs);
        }
    }

    @Test
    public void update() throws SQLException {
        Connection conn = null;
        Statement st = null;
        ResultSet rs = null;
        try {
            conn = JdbcUtils.getConnection();
            st = conn.createStatement();
            String sql = "update users set name = 'fff' where id = '4'";
            int num = st.executeUpdate(sql);
            if (num > 0) {
                System.out.println("更新成功!!!");
            }

        } finally {
            JdbcUtils.release(conn, st, rs);
        }
    }

    @Test
    public void delete() throws SQLException {
        Connection conn = null;
        Statement st = null;
        ResultSet rs = null;
        try {
            conn = JdbcUtils.getConnection();
            st = conn.createStatement();
            String sql = "delete from users where id='4'";
            int num = st.executeUpdate(sql);
            if (num > 0) {
                System.out.println("删除成功!!!");
            }
        } finally {
            JdbcUtils.release(conn, st, rs);
        }
    }

    @Test
    public void find() throws SQLException {
        Connection conn = null;
        Statement st = null;
        ResultSet rs = null;
        try {
            conn = JdbcUtils.getConnection();
            st = conn.createStatement();
            String sql = "select * from users where id='2'";
            rs = st.executeQuery(sql);
            if (rs != null) {
                System.out.println("查询成功!!!");
            }
        } finally {
            JdbcUtils.release(conn, st, rs);
        }
    }

    @Test
    public void getAll() throws SQLException {
        Connection conn = null;
        Statement st = null;
        ResultSet rs = null;
        try {
            conn = JdbcUtils.getConnection();
            st = conn.createStatement();
            String sql = "select id,name,password,email,birthday from users";
            rs = st.executeQuery(sql);
            List list = new ArrayList();

            while (rs.next()) {
                User user = new User();
                user.setId(rs.getInt("id"));
                user.setName(rs.getString("name"));
                user.setPassword(rs.getString("password"));
                user.setEmail(rs.getString("email"));
                user.setBirthday(rs.getDate("birthday"));
                list.add(user);
            }

            System.out.println(list);
        } finally {
            JdbcUtils.release(conn, st, rs);
        }
    }
}

db.properties资源文件:

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/day14
username=root
password=root

cn.wk.utils.JdbcUtils工具类:

package cn.wk.utils;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

public class JdbcUtils {

    private static Properties config = new Properties();

    static {
        try {
            // 读配置文件 db.properties
            config.load(JdbcUtils.class.getClassLoader().getResourceAsStream(
                    "db.properties"));
            Class.forName(config.getProperty("driver"));
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e); // 异常转换成错误
        }
    }

    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(config.getProperty("url"),
                config.getProperty("username"), config.getProperty("password"));
    }

    public static void release(Connection conn, Statement st, ResultSet rs) {
        // 模板代码
        if (rs != null) {
            try {
                rs.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            rs = null;
        }
        if (st != null) {
            try {
                st.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            st = null;
        }
        if (conn != null) {
            try {
                conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

5. 切换到 oracle 完成 crud

更换db.properties文件信息为 oracle 的,几乎不用改程序,就能够用oracle作crud。
为保证程序有移植性,即想作到不依赖具体的数据库管理系统,那么,Connection 等对象就用 sun 公司的接口,而不能用具体数据库的相应对象!如:咱们导入的是java.sql.Connection

6. 用 jdbc 改造用户模块

准备工做:
复制 day09_user 工程,命名为 day14_user,并点击该工程属性,选 web 选项, 将 Web Context-root 改成/day09_user

UserDaoJdbcImpl代码以下:

package cn.wk.dao.impl;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;

import cn.wk.dao.UserDao;
import cn.wk.domain.User;
import cn.wk.utils.JdbcUtils;

public class UserDaoJdbcImpl implements UserDao {

    @Override
    public void add(User user) {
        Connection conn = null;
        Statement st = null;
        ResultSet rs = null;

        try {
            conn = JdbcUtils.getConnection();
            st = conn.createStatement(); // conn.preparedment() 用占位符替换位置
            String sql = "insert into users(id,username,password,email,birthday,nickname)"
                    + "values('"
                    + user.getId()
                    + "','"
                    + user.getUsername()
                    + "','"
                    + user.getPassword()
                    + "','"
                    + user.getEmail()
                    + "','"
                    + user.getBirthday().toLocaleString()
                    + "','"
                    + user.getNickname() + "')";
            int num = st.executeUpdate(sql);
            if (num < 1) {
                throw new RuntimeException("注册用户失败!!!");
            }

        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            JdbcUtils.release(conn, st, rs);
        }
    }

    @Override
    public User find(String username, String password) {
        Connection conn = null;
        Statement st = null;
        ResultSet rs = null;

        try {
            conn = JdbcUtils.getConnection();
            st = conn.createStatement();
            String sql = "select * from users where username='" + username
                    + "' and password='" + password + "'";
            rs = st.executeQuery(sql);
            if (rs.next()) {
                User user = new User();
                user.setBirthday(rs.getDate("birthday"));
                user.setEmail(rs.getString("email"));
                user.setId(rs.getString("id"));
                user.setNickname(rs.getString("nickname"));
                user.setPassword(rs.getString("password"));
                user.setUsername(rs.getString("username"));
                return user;
            }
            return null;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            JdbcUtils.release(conn, st, rs);
        }
    }

    @Override
    public boolean find(String username) {
        Connection conn = null;
        Statement st = null;
        ResultSet rs = null;

        try {
            conn = JdbcUtils.getConnection();
            st = conn.createStatement();
            String sql = "select * from users where username='" + username
                    + "'";
            rs = st.executeQuery(sql);
            if (rs.next()) {
                return true;
            }
            return false;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            JdbcUtils.release(conn, st, rs);
        }
    }
}

其余的不写了,好累。(2018年5月30日 23:00 于 潍坊科技学院软件学院 315 办公室)

7. dao 工厂和预防 sql 注入

为使 service 层的 UserDao 与 实际的实现解耦,需专门创建 dao工厂类,由该工厂读取dao.properties这个配置文件,实现调用具体的 UserDao 实现,如调用UserDaoJdbcImplUserDaoXmlImpl等其余实现,以下图所示:
5

因为有可能有不少工厂,故创建cn.wk.factory包来存放工厂类。此时,咱们在该包内创建DaoFactory类,以下:

package cn.wk.factory;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class DaoFactory {

    private Properties daoConfig = new Properties();

    // 工厂是单例的,即全部的dao只由一个工厂来生产
    // new 对象同时,加载配置文件
    private DaoFactory() {
        InputStream in = DaoFactory.class.getClassLoader().getResourceAsStream(
                "dao.properties");
        try {
            daoConfig.load(in);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    private static DaoFactory instance = new DaoFactory();
    public static DaoFactory getInstance(){
        return instance;
    }

    // 未来可能产生多个 不一样类型的 dao,故此处用泛型

    //                  若给   UserDao.class,本方法就产生UserDao的dao
    //                      DepartmentDao.class 同上
    // 你给我一个接口类型, 我给你返回一个接口实现
    // 为避免调用者强转,此处应该用泛型

    // 泛型的学习,重点:
    // Class<T> : 人家传进来什么类型, T就表明什么类型
    // <T> T : <T> 是类型声明, T 是返回的类型
    public <T> T createDao(Class<T> clazz){

        // 1. 获得传进来的接口名称
        // clazz.getName()  //cn.wk.dao.UserDao
        String name = clazz.getSimpleName();
        String className = daoConfig.getProperty(name);
        try {
            // 加载类,并产生实例
            T dao = (T)Class.forName(className).newInstance();
            return dao;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

还需建dao.properties配置文件,以下:

UserDao=cn.wk.dao.impl.UserDaoJdbcImpl

修改BusinessServiceImpl以下:

// private UserDao dao = new UserDaoJdbcImpl(); // 可用工厂模式 或 spring 解耦,之后解耦

// 利用工厂,使得业务类,无代码依赖于具体实现,已彻底解耦
private UserDao dao = DaoFactory.getInstance().createDao(UserDao.class);

通过改造,系统结构很是好,好灵活,以下图:
6

关于异常:
应该每一层都自定义异常,以便抛出异常时,能快速定位是哪层出现异常。
对待异常的态度,方立勋遵循了 Spring 做者的理念,即:
你是否但愿上层处理本异常,若但愿,则抛出 checked 异常,不然抛出 unchecked 异常。有时,咱们就想对上层造麻烦,担忧上层忘记处理本层某异常,那就不用犹豫,抛 checked 异常便可!

7.1 preparedStatement 对象

statement 和 preparedStatement 的区别:

  1. preparedStatement 是 statement 的崽;
  2. preparedStatement 能够防止sql注入问题;
  3. preparedStatement 会对sql语句进行预编译,以减轻数据库服务器的压力。

preparedStatement 使用的代码片断,通常就用 preparedStatement 而不用 statement,以下:

String url = "jdbc:mysql:///day14";
String username = "root";
String password = "root";

Connection conn = null;
preparedStatement st = null;
ResultSet rs = null;

Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection(url, username, password);
String sql = "select * from users where username=?"; // ?占位符
st = conn.preparedStatement(sql);
st.setString(1, username); // 填补?号

rs = st.executeQuery();
if(rs.next()){
  rs.close();
  st.close();
  conn.close();
  return true;
}
rs.close();
st.close();
conn.close();
return false;

8. jdbc 实现客户关系管理案例

要实现一个客户关系管理系统,客户信息 customer 表以下:

字段名 类型
id varchar(40)
name varchar(20)
gender varchar(4)
birthday date
cellphone varchar(20)
email varchar(40)
preference varchar(100)
type varchar(40)
description varchar(255)
1.搭建环境
    1.1 导开发包
        mysql驱动
        beanUtils
        log4j开发
        jstl开发包

    1.2 建立组织程序的包
        cn.wk.domain
        cn.wk.dao
        cn.wk.dao.impl
        cn.wk.service
        cn.wk.service.impl
        cn.wk.web.controller
        cn.wk.web.UI
        cn.wk.utils
        cn.wk.exception
        junit.test

        WEB-INF/jsp

    1.3 为应用建立相应库和表
        create database day14_customer character set utf8 collate utf8_general_ci;
        use day14_customer;
        create table customer
        (
            id varchar(40) primary key,
            name varchar(40) not null,
            gender varchar(4) not null,
            birthday date,
            cellphone varchar(20),
            email varchar(40),
            preference varchar(255),
            type varchar(100) not null,
            description varchar(255)
        );

2.建实体

3.写dao

4.写service

5.写web

根据应用建立数据库及表,根据表建立 javabean,以下:

package cn.wk.domain;

import java.util.Date;

public class Customer {
    private String id;
    private String name;
    private String gender;
    private Date birthday;
    private String cellphone;
    private String email;
    private String preference;
    private String type;
    private String description;

    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getGender() {
        return gender;
    }
    public void setGender(String gender) {
        this.gender = gender;
    }
    public Date getBirthday() {
        return birthday;
    }
    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }
    public String getCellphone() {
        return cellphone;
    }
    public void setCellphone(String cellphone) {
        this.cellphone = cellphone;
    }
    public String getPreference() {
        return preference;
    }
    public void setPreference(String preference) {
        this.preference = preference;
    }
    public String getType() {
        return type;
    }
    public void setType(String type) {
        this.type = type;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
}

要链接数据库,则建立连数据库的工具类,这算是一种模板代码,cn.wk.utils.JdbcUtils,以下:

package cn.wk.utils;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

public class JdbcUtils {

    private static Properties config = new Properties();

    static {
        try {
            // 读配置文件 db.properties
            config.load(JdbcUtils.class.getClassLoader().getResourceAsStream(
                    "db.properties"));
            Class.forName(config.getProperty("driver"));
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e); // 异常转换成错误
        }
    }

    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(config.getProperty("url"),
                config.getProperty("username"), config.getProperty("password"));
    }

    public static void release(Connection conn, Statement st, ResultSet rs) {
        // 模板代码
        if (rs != null) {
            try {
                rs.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            rs = null;
        }
        if (st != null) {
            try {
                st.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            st = null;
        }
        if (conn != null) {
            try {
                conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

连数据库时,需用到配置文件db.properties,它在 src 目录下,代码以下:

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/day14_customer
username=root
password=root

再建立操做数据库进行 crud 的实现类cn.wk.dao.impl.CustomerDaoImpl,以下:

package cn.wk.dao.impl;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

import cn.wk.dao.CustomerDao;
import cn.wk.domain.Customer;
import cn.wk.exception.DaoException;
import cn.wk.utils.JdbcUtils;

public class CustomerDaoImpl implements CustomerDao {

    @Override
    public void add(Customer c) {
        Connection conn = null;
        PreparedStatement st = null;
        ResultSet rs = null;

        try {
            conn = JdbcUtils.getConnection();
            String sql = "insert into customer (id,name,gender,birthday,cellphone,email,preference,type,description) values (?,?,?,?,?,?,?,?,?)";
            st = conn.prepareStatement(sql);
            st.setString(1, c.getId());
            st.setString(2, c.getName());
            st.setString(3, c.getGender());
            st.setDate(4, new java.sql.Date(c.getBirthday().getTime()));
            st.setString(5, c.getCellphone());
            st.setString(6, c.getEmail());
            st.setString(7, c.getPreference());
            st.setString(8, c.getType());
            st.setString(9, c.getDescription());

            st.executeUpdate();

        } catch (Exception e) {
            throw new DaoException(e);
        } finally {
            JdbcUtils.release(conn, st, rs);
        }
    }

    @Override
    public void update(Customer c) {
        Connection conn = null;
        PreparedStatement st = null;
        ResultSet rs = null;

        try {
            conn = JdbcUtils.getConnection();
            String sql = "udate customer set name=?,gender=?,birthday=?,cellphone=?,email=?,preference=?,type=?,description=? where id=?";
            st = conn.prepareStatement(sql);
            st.setString(1, c.getName());
            st.setString(2, c.getGender());
            st.setDate(3, new java.sql.Date(c.getBirthday().getTime()));
            st.setString(4, c.getCellphone());
            st.setString(5, c.getEmail());
            st.setString(6, c.getPreference());
            st.setString(7, c.getType());
            st.setString(8, c.getDescription());
            st.setString(9, c.getId());
            st.executeUpdate();
        } catch (Exception e) {
            throw new DaoException(e);
        } finally {
            JdbcUtils.release(conn, st, rs);
        }
    }

    @Override
    public void delete(String id) {
        Connection conn = null;
        PreparedStatement st = null;
        ResultSet rs = null;

        try {
            conn = JdbcUtils.getConnection();
            String sql = "delete from customer where id=?";
            st = conn.prepareStatement(sql);
            st.setString(1, id);
            st.executeUpdate();
        } catch (Exception e) {
            throw new DaoException(e);
        } finally {
            JdbcUtils.release(conn, st, rs);
        }
    }

    @Override
    public Customer find(String id) {
        Connection conn = null;
        PreparedStatement st = null;
        ResultSet rs = null;

        try {
            conn = JdbcUtils.getConnection();
            String sql = "select * from customer where id=?";
            st = conn.prepareStatement(sql);
            st.setString(1, id);
            rs = st.executeQuery();
            if (rs.next()) {
                Customer c = new Customer();
                c.setBirthday(rs.getDate("birthday"));
                c.setCellphone(rs.getString("cellphone"));
                c.setDescription(rs.getString("description"));
                c.setEmail(rs.getString("email"));
                c.setGender(rs.getString("gender"));
                c.setId(rs.getString("id"));
                c.setName(rs.getString("name"));
                c.setPreference(rs.getString("preference"));
                c.setType(rs.getString("type"));
                return c;
            }
        } catch (Exception e) {
            throw new DaoException(e);
        } finally {
            JdbcUtils.release(conn, st, rs);
        }
        return null;
    }

    @Override
    public List<Customer> getAll() {
        Connection conn = null;
        PreparedStatement st = null;
        ResultSet rs = null;

        try {
            conn = JdbcUtils.getConnection();
            String sql = "select * from customer";
            st = conn.prepareStatement(sql);
            rs = st.executeQuery();

            List<Customer> list = new ArrayList<Customer>();
            while (rs.next()) {
                Customer c = new Customer();
                c.setBirthday(rs.getDate("birthday"));
                c.setCellphone(rs.getString("cellphone"));
                c.setDescription(rs.getString("description"));
                c.setEmail(rs.getString("email"));
                c.setGender(rs.getString("gender"));
                c.setId(rs.getString("id"));
                c.setName(rs.getString("name"));
                c.setPreference(rs.getString("preference"));
                c.setType(rs.getString("type"));
                list.add(c);
            }
            return list;
        } catch (Exception e) {
            throw new DaoException(e);
        } finally {
            JdbcUtils.release(conn, st, rs);
        }
    }
}

再对操做数据库的实现类cn.wk.dao.impl.CustomerDaoImpl抽取成接口cn.wk.dao.CustomerDao,以下:

package cn.wk.dao;
import java.util.List;
import cn.wk.domain.Customer;

public interface CustomerDao {
    public abstract void add(Customer c);
    public abstract void update(Customer c);
    public abstract void delete(String id);
    public abstract Customer find(String id);
    public abstract List<Customer> getAll();
}

这时,注意到每层都应创建本身的异常类,以方便未来发生异常时,方便程序员了解究竟是哪层发生异常。故创建cn.wk.exception.DaoException,并声明该异常类为RuntimeException,同时继承父类的全部方法,以下:

package cn.wk.exception;
public class DaoException extends RuntimeException {
    public DaoException() {}
    public DaoException(String message) {super(message);}
    public DaoException(Throwable cause) {super(cause);}
    public DaoException(String message, Throwable cause) {super(message, cause);}
    public DaoException(String message, Throwable cause,
            boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

接着,想到本工程的业务仅仅是对客户信息进行 crud,业务逻辑超级简单。因此,咱们能够当即创建业务层实现cn.wk.service.impl.BusinessServiceImpl,以下(注意:若业务逻辑很复杂,本层代码也会很复杂):

package cn.wk.service.impl;

import java.util.List;

import cn.wk.dao.CustomerDao;
import cn.wk.dao.impl.CustomerDaoImpl;
import cn.wk.domain.Customer;
import cn.wk.service.BusinessService;

// 薄薄的业务层
public class BusinessServiceImpl implements BusinessService {

    private CustomerDao dao = new CustomerDaoImpl();

    @Override
    public void addCustomer(Customer c){
        dao.add(c);
    }

    @Override
    public void updateCustomer(Customer c){
        dao.update(c);
    }

    @Override
    public void deleteCustomer(String id){
        dao.delete(id);
    }

    @Override
    public Customer findCustomer(String id){
        return dao.find(id);
    }

    @Override
    public List<Customer> getAllCustormer(){
        return dao.getAll();
    }
}

而后当即进行业务层实现的抽取,造成接口cn.wk.service.BusinessService

package cn.wk.service;
import java.util.List;
import cn.wk.domain.Customer;
public interface BusinessService {
    public abstract void addCustomer(Customer c);
    public abstract void updateCustomer(Customer c);
    public abstract void deleteCustomer(String id);
    public abstract Customer findCustomer(String id);
    public abstract List<Customer> getAllCustormer();
}

接下来,和 web 相关,也就是和用户相关的层的代码会较复杂,要当心了。
首先考虑首页 index.jsp,本例用了分帧技术,涉及了 index.jsp 和 head.jsp,想达到以下效果:
7

index.jsp 以下:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>首页</title>
  </head>

  <frameset rows="20%,*">
    <frame name="head" src="${pageContext.request.contextPath}/head.jsp">
    <frame name="main">
  </frameset>
</html>

head.jsp 以下:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>head</title>
  </head>

  <body style="text-align:center;">
    <h1>XXX客户关系管理系统</h1>
    <br>   

    <a href="${pageContext.request.contextPath}/servlet/AddCustomerServlet" target="main">添加客户</a>
    <a href="${pageContext.request.contextPath}/servlet/ListCustomerServlet" target="main">查看客户</a>
  </body>
</html>

注意到 head.jsp 中的2个超连接,结果显示在 main 那个帧里。
根据上述的2个超连接,分别编写那2个控制器servlet程序。
先看添加客户按钮的超连接cn.wk.web.controller.AddCustomerServlet,当客户点击“添加客户”按钮时,发出的是 get 请求, 由AddCustomerServlet的 doGet 方法处理,该方法又把请求转发给/WEB-INF/jsp/addcustomer.jsp去显示表单,用户填写完表单后,再用action="${pageContext.request.contextPath}/servlet/AddCustomerServlet" method="post"使用 post 请求又链接回cn.wk.web.controller.AddCustomerServlet这个servlet控制器,该控制器用 doPost 方法接受请求并处理(就是把客户填写的表单封装成一个 Customer 对象,并转交给业务层处理,业务层将该对象转交给dao,由dao负责将对 Customer 对象存入数据库),其中涉及到 message 的信息传送,以表达“添加成功或失败”信息,并回显给客户。

cn.wk.web.controller.AddCustomerServlet以下:

package cn.wk.web.controller;

import java.io.IOException;

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

import cn.wk.domain.Customer;
import cn.wk.service.BusinessService;
import cn.wk.service.impl.BusinessServiceImpl;
import cn.wk.utils.Globals;
import cn.wk.utils.WebUtils;

public class AddCustomerServlet extends HttpServlet {

    // 给用户提供一个添加界面
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        // 信息带给jsp
        req.setAttribute("genders", Globals.genders);
        req.setAttribute("preferences", Globals.preferences);
        req.setAttribute("types", Globals.types);

        // 视图
        req.getRequestDispatcher("/WEB-INF/jsp/addcustomer.jsp").forward(req,
                resp);
    }

    // 处理用户的添加请求
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        try {
            req.setCharacterEncoding("UTF-8");
            // 表单校验

            // 把表单数据封装到 customer 对象中 , 用工具类 WebUtils
            Customer c = WebUtils.request2Bean(req, Customer.class);
            c.setId(WebUtils.generateID());

            BusinessService service = new BusinessServiceImpl();
            service.addCustomer(c);
            req.setAttribute("message", "添加成功!!");

        } catch (Exception e) {
            e.printStackTrace();
            req.setAttribute("message", "添加失败!!");
        }
        req.getRequestDispatcher("/message.jsp").forward(req, resp);
    }
}

控制层的cn.wk.web.controller.AddCustomerServlet还引入了cn.wk.utils.Globals这个工具类,该类目的是不想把genders, preferences, types写死,经过修改该类,达到直接控制那3个属性的目的,代码以下:

package cn.wk.utils;

public class Globals {
    public static String genders[] = { "男", "女", "人妖" };
    public static String preferences[] = { "唱歌", "跳舞", "桑拿", "打麻将", "看凤姐",
            "夜生活", "xxx" };
    public static String types[] = { "vip客户", "重点客户", "普通客户" };
}

/WEB-INF/jsp/addcustomer.jsp以下(涉及一个显示日期的js控件/WEB-INF/js/ShowCalendar.js):
该代码涉及了一个makepre()的 js 函数,功能是把客户的多个preference组合成一个字符串,并弄出一个 隐式输入项 送入要提交的表单中。makepre()函数涉及了 javascript 的 DOM 编程,挺有意思的。

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>添加用户的视图</title>
    <script type="text/javascript" src="${pageContext.request.contextPath }/js/ShowCalendar.js"></script>
    <script type="text/javascript">
        function makepre(){
            var pres = document.getElementsByName("pre");
            var preference = "";
            for(var i = 0; i < pres.length; i++){
                var input = pres[i];
                if(input.checked==true){
                    preference = preference + input.value + ",";
                }
            }
            // 组装字符串   跳舞,打麻将,看凤姐
            preference = preference.substr(0, preference.length - 1);

            var form = document.getElementById("form");
            var input = document.createElement("input");
            input.type = "hidden";
            input.name = "preference";
            input.value = preference;

            form.appendChild(input);
            return true;
        }
    </script>
</head>

<body style="text-align:center;">
    <br />
    <form id="form"
        action="${pageContext.request.contextPath}/servlet/AddCustomerServlet"
        method="post" onsubmit="return makepre()">
        <!-- js代码,当按submit按钮时,就调用makepre()方法 -->

        <table border="1" width="30%">
            <tr>
                <td>客户姓名</td>
                <td><input type="text" name="name"></td>
            </tr>

            <tr>
                <td>性别</td>
                <td><c:forEach var="gender" items="${genders}">
                        <input type="radio" name="gender" value="${gender}"> ${gender}
                </c:forEach></td>
            </tr>

            <tr>
                <td>生日</td>
                <td><input type="text" name="birthday"
                    onClick="showCalendar(this.id)" id="birthday"></td>
            </tr>

            <tr>
                <td>手机</td>
                <td><input type="text" name="cellphone"></td>
            </tr>

            <tr>
                <td>邮箱</td>
                <td><input type="text" name="email"></td>
            </tr>

            <tr>
                <td>爱好</td>
                <td><c:forEach var="p" items="${preferences}">
                        <input type="checkbox" name="pre" value="${p}">${p}
                </c:forEach></td>
            </tr>

            <tr>
                <td>客户类型</td>
                <td><c:forEach var="t" items="${types}">
                        <input type="radio" name="type" value="${t}">${t}
                </c:forEach></td>
            </tr>

            <tr>
                <td>客户备注</td>
                <td><textarea rows="5" cols="100" name="description"></textarea>
                </td>
            </tr>

            <tr>
                <td><input type="reset" value="重置"></td>
                <td><input type="submit" value="添加客户"></td>
            </tr>
        </table>
    </form>
</body>
</html>

/message.jsp以下:

我也把那个显示日期的 js 控件/WEB-INF/js/ShowCalendar.js代码贴出来:

// 日期选择
// By Ziyue(http://www.web-v.com/)
// 使用方法:
// <script type="text/javascript" src="${pageContext.request.contextPath }/js/ShowCalendar.js"></script>
// <input name="birthday" type="text" id="birthday" title="点击选择" onClick="showCalendar(this.id)">

var today;
document
        .writeln("<div id='Calendar' style='position:absolute; z-index:1; visibility: hidden; filter:\"progid:DXImageTransform.Microsoft.Shadow(direction=135,color=#999999,strength=3)\"'></div>");

function getDays(month, year) {
    var daysInMonth = new Array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
    // 下面的这段代码是判断当前是不是闰年的
    if (1 == month)
        return ((0 == year % 4) && (0 != (year % 100))) || (0 == year % 400) ? 29
                : 28;
    else
        return daysInMonth[month];
}

function getToday() {
    // 获得今天的年,月,日
    this.now = new Date();
    this.year = this.now.getFullYear();
    this.month = this.now.getMonth();
    this.day = this.now.getDate();
}

function getStringDay(str) {
    // 获得输入框的年,月,日
    var str = str.split("-");

    this.now = new Date(parseFloat(str[0]), parseFloat(str[1]) - 1,
            parseFloat(str[2]));
    this.year = this.now.getFullYear();
    this.month = this.now.getMonth();
    this.day = this.now.getDate();
}

function newCalendar() {
    var parseYear = parseInt(document.all.Year.options[document.all.Year.selectedIndex].value);
    var newCal = new Date(parseYear, document.all.Month.selectedIndex, 1);
    var day = -1;
    var startDay = newCal.getDay();
    var daily = 0;

    if ((today.year == newCal.getFullYear())
            && (today.month == newCal.getMonth()))
        day = today.day;

    var tableCal = document.all.calendar;
    var intDaysInMonth = getDays(newCal.getMonth(), newCal.getFullYear());

    for (var intWeek = 1; intWeek < tableCal.rows.length; intWeek++)
        for (var intDay = 0; intDay < tableCal.rows[intWeek].cells.length; intDay++) {
            var cell = tableCal.rows[intWeek].cells[intDay];
            if ((intDay == startDay) && (0 == daily))
                daily = 1;

            if (day == daily) // 今天,调用今天的Class
            {
                cell.style.background = '#6699CC';
                cell.style.color = '#FFFFFF';
                // cell.style.fontWeight='bold';
            } else if (intDay == 6) // 周六
                cell.style.color = 'green';
            else if (intDay == 0) // 周日
                cell.style.color = 'red';

            if ((daily > 0) && (daily <= intDaysInMonth)) {
                cell.innerText = daily;
                daily++;
            } else
                cell.innerText = "";
        }
}

function GetDate(InputBox) {
    var sDate;
    // 这段代码处理鼠标点击的状况
    if (event.srcElement.tagName == "TD")
        if (event.srcElement.innerText != "") {
            sDate = document.all.Year.value + "-" + document.all.Month.value
                    + "-" + event.srcElement.innerText;
            eval("document.all." + InputBox).value = sDate;
            HiddenCalendar();
        }
}

function HiddenCalendar() {
    // 关闭选择窗口
    document.all.Calendar.style.visibility = 'hidden';
}

function showCalendar(InputBox) {
    var months = new Array("一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月",
            "九月", "十月", "十一月", "十二月");
    var days = new Array("日", "一", "二", "三", "四", "五", "六");

    var x, y, intLoop, intWeeks, intDays;
    var DivContent;
    var year, month, day;
    var o = eval("document.all." + InputBox);
    var thisyear; // 真正的今年年份

    thisyear = new getToday();
    thisyear = thisyear.year;

    today = o.value;
    if (isDate(today))
        today = new getStringDay(today);
    else
        today = new getToday();

    // 显示的位置
    x = o.offsetLeft;
    y = o.offsetTop;
    while (o = o.offsetParent) {
        x += o.offsetLeft;
        y += o.offsetTop;
    }
    document.all.Calendar.style.left = x + 2;
    document.all.Calendar.style.top = y + 20;
    document.all.Calendar.style.visibility = "visible";

    // 下面开始输出日历表格(border-color:#9DBAF7)
    DivContent = "<table border='0' cellspacing='0' style='border:1px solid #0066FF; background-color:#EDF2FC'>";
    DivContent += "<tr>";
    DivContent += "<td style='border-bottom:1px solid #0066FF; background-color:#C7D8FA'>";

    // 年
    DivContent += "<select name='Year' id='Year' onChange='newCalendar()' style='font-family:Verdana; font-size:12px'>";
    for (intLoop = thisyear - 35; intLoop < (thisyear + 2); intLoop++)
        DivContent += "<option value= " + intLoop + " "
                + (today.year == intLoop ? "Selected" : "") + ">" + intLoop
                + "</option>";
    DivContent += "</select>";

    // 月
    DivContent += "<select name='Month' id='Month' onChange='newCalendar()' style='font-family:Verdana; font-size:12px'>";
    for (intLoop = 0; intLoop < months.length; intLoop++)
        DivContent += "<option value= " + (intLoop + 1) + " "
                + (today.month == intLoop ? "Selected" : "") + ">"
                + months[intLoop] + "</option>";
    DivContent += "</select>";

    DivContent += "</td>";

    DivContent += "<td style='border-bottom:1px solid #0066FF; background-color:#C7D8FA; font-weight:bold; font-family:Wingdings 2,Wingdings,Webdings; font-size:16px; padding-top:2px; color:#4477FF; cursor:hand' align='center' title='关闭' onClick='javascript:HiddenCalendar()'>S</td>";
    DivContent += "</tr>";

    DivContent += "<tr><td align='center' colspan='2'>";
    DivContent += "<table id='calendar' border='0' width='100%'>";

    // 星期
    DivContent += "<tr>";
    for (intLoop = 0; intLoop < days.length; intLoop++)
        DivContent += "<td align='center' style='font-size:12px'>"
                + days[intLoop] + "</td>";
    DivContent += "</tr>";

    // 天
    for (intWeeks = 0; intWeeks < 6; intWeeks++) {
        DivContent += "<tr>";
        for (intDays = 0; intDays < days.length; intDays++)
            DivContent += "<td onClick='GetDate(\""
                    + InputBox
                    + "\")' style='cursor:hand; border-right:1px solid #BBBBBB; border-bottom:1px solid #BBBBBB; color:#215DC6; font-family:Verdana; font-size:12px' align='center'></td>";
        DivContent += "</tr>";
    }
    DivContent += "</table></td></tr></table>";

    document.all.Calendar.innerHTML = DivContent;
    newCalendar();
}

function isDate(dateStr) {
    var datePat = /^(\d{4})(\-)(\d{1,2})(\-)(\d{1,2})$/;
    var matchArray = dateStr.match(datePat);
    if (matchArray == null)
        return false;
    var month = matchArray[3];
    var day = matchArray[5];
    var year = matchArray[1];
    if (month < 1 || month > 12)
        return false;
    if (day < 1 || day > 31)
        return false;
    if ((month == 4 || month == 6 || month == 9 || month == 11) && day == 31)
        return false;
    if (month == 2) {
        var isleap = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
        if (day > 29 || (day == 29 && !isleap))
            return false;
    }
    return true;
}

把 request 中表单信息封装成javabean需创建一个cn.wk.utils.WebUtils工具类。因需把request中的一个 String 类型的参数birthday=1999-01-01转换成java.utils.Date对象,需注册一个转换器,这玩意对我来讲很陌生,代码以下:

package cn.wk.utils;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.UUID;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.Converter;

public class WebUtils {
    public static <T> T request2Bean(HttpServletRequest request,
            Class<T> beanClass) {
        try {
            T bean = beanClass.newInstance();

            // 获得 request 里全部数据
            Map map = request.getParameterMap();

            // 填充
            // map{name=aa,password=bb,birthday=1999-01-01} 填充到bean
            // bean{name=aa,password=bb,birthday=Date}

            ConvertUtils.register(new Converter() {

                @Override
                // 字符串 转成 java.util.Date 返回
                public Object convert(Class type, Object value) {
                    if (value == null) {
                        return null;
                    }

                    String str = (String) value;
                    if (str.trim().equals("")) {
                        return null;
                    }

                    SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
                    try {
                        return df.parse(str);
                    } catch (ParseException e) {
                        throw new RuntimeException(e);
                    }
                }

            }, Date.class);

            BeanUtils.populate(bean, map);
            return bean;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static String generateID(){
        return UUID.randomUUID().toString();        
    }
}

head.jsp 中的另外一个按钮“查看客户”,跳转到一个控制层的servlet上,cn.wk.web.controller.ListCustomerServlet代码以下:

package cn.wk.web.controller;

import java.io.IOException;
import java.util.List;

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

import cn.wk.service.BusinessService;
import cn.wk.service.impl.BusinessServiceImpl;

// 获得全部客户显示
public class ListCustomerServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        try {
            BusinessService service = new BusinessServiceImpl();
            List list = service.getAllCustormer();
            req.setAttribute("list", list);
            req.getRequestDispatcher("/WEB-INF/jsp/listcustomer.jsp").forward(
                    req, resp);
        } catch (Exception e) {
            e.printStackTrace();
            req.setAttribute("message", "查看客户失败!!");
            req.getRequestDispatcher("/message.jsp").forward(req, resp);
        }
    }

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

经过上面的servlet,除了发生异常时发送“查看客户失败!!”的信息到/message.jsp,无异常时将request转发给/WEB-INF/jsp/listcustomer.jsp,其代码以下:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>列出全部客户</title>
</head>

<body style="text-align: center;">
    <table frame="border" width="85%">
        <tr>
            <td>客户姓名</td>
            <td>性别</td>
            <td>生日</td>
            <td>手机</td>
            <td>邮箱</td>
            <td>爱好</td>
            <td>类型</td>
            <td>备注</td>
            <td>操做</td>
        </tr>

        <c:forEach var="c" items="#{requestScope.list}">
            <tr>
                <td>${c.name }</td>
                <td>${c.gender }</td>
                <td>${c.birthday }</td>
                <td>${c.cellphone }</td>
                <td>${c.email }</td>
                <td>${c.preference }</td>
                <td>${c.type }</td>
                <td>${c.description }</td>
                <td>
                    <a href="#">修改</a>
                    <a href="#">删除</a>
                </td>
            </tr>
        </c:forEach>
    </table>
</body>
</html>

上述 jsp 页面仍会显示在 index.jsp 中name="main"的分帧里,缘由是在 head.jsp 中有这样的定义:

<a href="${pageContext.request.contextPath}/servlet/ListCustomerServlet" target="main">查看客户</a>

即用户点击查看客户按钮,跳转到控制层的ListCustomerServlet这个servlet上,而后request又由该servlet转发到/WEB-INF/jsp/listcustomer.jsp这个数据展现页面上,而这个展现页面仍受 head.jsp 中的target="main"的控制,最终拼接在 index.jsp 页面指定的位置上。 真够复杂的了。

相关文章
相关标签/搜索