JavaWeb
- 查看端口占用
- windows:netstat -ano | findstr :7777
- linux:lsof -i:7777
MySQL
单行注释:-- 或者 #(MySQL特有)
多行注释:/* */
SQL分类
- DDL(Data Definition Language)数据定义语言,用来定义数据库对象:数据库,表,列等
- DML(Data Manipulation Language)数据操作语言,用来对数据库中表的数据进行增删改
- DQL(Data Query Language)数据查询语言,用来查询数据库中表的记录(数据)
- DCL(Data Control Language)数据控制语言,用来定义数据库的访问权限和安全级别,及创建用户
JDBC
用java操作数据库的API
获取数据库连接
要素一:Driver接口实现类
Driver接口介绍
- java.sql.Driver 接口是所有 JDBC 驱动程序需要实现的接口。这个接口是提供给数据库厂商使用的,不同数据库厂商提供不同的实现。
- 在程序中不需要直接去访问实现了 Driver 接口的类,而是由驱动程序管理器类(java.sql.DriverManager)去调用这些Driver实现。
- Oracle的驱动:oracle.jdbc.driver.OracleDriver
- mySql的驱动: com.mysql.jdbc.Driver
加载与注册JDBC驱动
- 加载驱动:加载 JDBC 驱动需调用 Class 类的静态方法 forName(),向其传递要加载的 JDBC 驱动的类名
- Class.forName(“com.mysql.jdbc.Driver”);
- 注册驱动:DriverManager 类是驱动程序管理器类,负责管理驱动程序
- 使用DriverManager.registerDriver(com.mysql.jdbc.Driver)来注册驱动
- 通常不用显式调用 DriverManager 类的 registerDriver() 方法来注册驱动程序类的实例,因为 Driver 接口的驱动程序类都包含了静态代码块,在这个静态代码块中,会调用 DriverManager.registerDriver() 方法来注册自身的一个实例。下面是MySQL的Driver实现类的源码:
package com.mysql.jdbc;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
要素二:URL
- JDBC URL 用于标识一个被注册的驱动程序,驱动程序管理器通过这个 URL 选择正确的驱动程序,从而建立到数据库的连接。
- JDBC URL的标准由三部分组成,各部分间用冒号分隔。
- jdbc:子协议:子名称
- 协议:JDBC URL中的协议总是jdbc
- 子协议:子协议用于标识一个数据库驱动程序
- 子名称:一种标识数据库的方法。子名称可以依不同的子协议而变化,用子名称的目的是为了定位数据库提供足够的信息。包含主机名(对应服务端的ip地址),端口号,数据库名
- 举例
jdbc:mysql://localhost:3306/dbtest
其中
jdbc --> 协议
mysql --> 子协议
localhost:3306/dbtest --> 子名称
localhost --> 主机名
3306 --> 端口号(MySQL默认端口号为3306)
dbtest --> 数据库名
- 几种常用数据库的 JDBC URL
- MySQL的连接URL编写方式:
- jdbc:mysql://主机名称:mysql服务端口号/数据库名称?参数=值&参数=值
- jdbc:mysql://localhost:3306/atguigu
- jdbc:mysql://localhost:3306/atguigu?useUnicode=true&characterEncoding=utf8(如果JDBC程序与服务器端的字符集不一致,会导致乱码,那么可以通过参数指定服务器端的字符集)
- jdbc:mysql://localhost:3306/atguigu?user=root&password=123456
- Oracle 9i的连接URL编写方式:
- jdbc:oracle:thin:@主机名称:oracle服务端口号:数据库名称
- jdbc:oracle:thin:@localhost:1521:atguigu
- SQLServer的连接URL编写方式:
- jdbc:sqlserver://主机名称:sqlserver服务端口号:DatabaseName=数据库名称
- jdbc:sqlserver://localhost:1433:DatabaseName=atguigu
- MySQL的连接URL编写方式:
要素三:用户名和密码
- user,password可以用“属性名=属性值”方式告诉数据库
- 可以调用 DriverManager 类的 getConnection() 方法建立到数据库的连接
数据库连接方式举例
连接方式一
@Test
public void testConnection01() throws SQLException {
//1. 提供java.sql.Driver接口实现类的对象
Driver driver = new com.mysql.jdbc.Driver();
//2. 提供url,指明具体操作的数据
String url = "jdbc:mysql://localhost:13306/atguigudb";
//3. 提供Properties对象,指明用户名和密码
Properties info = new Properties();
info.setProperty("user","root");
info.setProperty("password","yourPassword");
//4. 调用driver的connect(),获取连接
Connection connect = driver.connect(url, info);
System.out.println(connect);
}
说明:上述代码中显式出现了第三方数据库的API
连接方式二
@Test
public void testConnection02() throws Exception {
//1. 实例化Driver
String className = "com.mysql.jdbc.Driver";
Class clazz = Class.forName(className);
Driver driver = (Driver) clazz.newInstance();
//2. 提供url,指明具体操作的数据
String url = "jdbc:mysql://localhost:13306/atguigudb";
//3. 提供Properties对象,指明用户名和密码
Properties info = new Properties();
info.setProperty("user","root");
info.setProperty("password","yourPassword");
//4. 调用driver的connect(),获取连接
Connection connect = driver.connect(url, info);
System.out.println(connect);
}
说明:相较于方式一,这里🚩使用反射实例化Driver,不在代码中体现第三方数据库的API。体现了面向接口编程思想。
连接方式三
@Test
public void testConnection03() throws Exception {
//1.数据库连接的4个基本要素:
String className = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:13306/atguigudb";
String user = "root";
String password = "yourPassword";
//2. 实例化Driver
Class clazz = Class.forName(className);
Driver driver = (Driver) clazz.newInstance();
//3. 注册驱动
DriverManager.registerDriver(driver);
//4. 获取连接
Connection connect = DriverManager.getConnection(url,user,password);
System.out.println(connect);
}
说明:使用DriverManager实现数据库的连接。体会获取连接必要的4个基本要素。
连接方式四
@Test
public void testConnection04() throws Exception {
//1.数据库连接的4个基本要素:
String className = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:13306/atguigudb";
String user = "root";
String password = "yourPassword";
//2. 加载驱动
Class.forName(className);
//3. 获取连接
Connection connect = DriverManager.getConnection(url,user,password);
System.out.println(connect);
}
public void t4() throws Exception
{
// String className = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306?useSSL=False";
String user = "cdd";
String password = " ";
// Class.forName(className);
Connection connect = DriverManager.getConnection(url, user, password);
System.out.println(connect);
}
说明:不必显式的注册驱动了。因为🚩在DriverManager的源码中已经存在静态代码块,实现了驱动的注册。
连接方式五(最终版)
@Test
public void testConnection05() throws Exception {
//1. 加载配置文件
InputStream is = ConnectionTest.class.getClassLoader().getResourceAsStream("jdbc.properties");
Properties properties = new Properties();
properties.load(is);
//2. 读取配置信息
String className = properties.getProperty("className");
String url = properties.getProperty("url");
String user = properties.getProperty("user");
String password = properties.getProperty("password");
//2. 加载驱动
Class.forName(className);
//4. 获取连接
Connection connect = DriverManager.getConnection(url,user,password);
System.out.println(connect);
}
- 🚩链接本机:url=jdbc:mysql:///?useSSL=False
- 其中,配置文件声明在工程的src目录下:【jdbc.properties】
user=root
password=yourpassword
url=jdbc:mysql://localhost:3306/test
driverClass=com.mysql.jdbc.Driver
- 说明:使用配置文件的方式保存配置信息,在代码中加载配置文件
- 使用配置文件的好处:
- 实现了代码和数据的分离,如果需要修改配置信息,直接在配置文件中修改,不需要深入代码
- 如果修改了配置信息,省去重新编译的过程。
使用PreparedStatement实现CRUD操作
操作和访问数据库
- 数据库连接被用于向数据库服务器发送命令和 SQL 语句,并接受数据库服务器返回的结果。其实一个数据库连接就是一个Socket连接。
- 在 java.sql 包中有 3 个接口分别定义了对数据库的调用的不同方式:
Statement
:用于执行静态 SQL 语句并返回它所生成结果的对象。PrepatedStatement
:SQL 语句被预编译并存储在此对象中,可以使用此对象多次高效地执行该语句。CallableStatement
:用于执行 SQL 存储过程
使用Statement操作数据表的弊端
- 通过调用
Connection
对象的createStatement()
方法创建该对象。该对象用于执行静态的 SQL 语句,并且返回执行结果。 - Statement 接口中定义了下列方法用于执行 SQL 语句:
int excuteUpdate(String sql) //执行更新操作INSERT、UPDATE、DELETE🚩返回受影响的条数
ResultSet executeQuery(String sql) //执行查询操作SELECT🚩返回结果集
- 但是使用Statement操作数据表存在弊端:
- 问题一:存在拼串操作,繁琐
- 问题二:存在SQL注入问题
- SQL 注入是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入数据中注入非法的 SQL 语句段或命令,从而利用系统的 SQL 引擎完成恶意行为的做法。
- 对于 Java 而言,要防范 SQL 注入,只要用 PreparedStatement(从Statement扩展而来) 取代 Statement 就可以了。
- SQL注入示例
StatementTest:
import org.junit.Test;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.sql.*;
import java.util.Properties;
public class StatementTest {
@Test
public void testLogin() {
String userName = "1' or ";
String password = "=1 or '1' = '1";
String sql = "select user,password from user_table where user = '" + userName + "' and password = '" + password + "'";
User user = get(sql, User.class);
if (user == null)
System.out.println("用户名或密码错误!");
else System.out.println("登陆成功!");
}
// 使用Statement实现对数据表的查询操作
public <T> T get(String sql, Class<T> clazz) {
T t = null;
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try {
// 1.加载配置文件
InputStream is = StatementTest.class.getClassLoader().getResourceAsStream("jdbc.properties");
Properties pros = new Properties();
pros.load(is);
// 2.读取配置信息
String user = pros.getProperty("user");
String password = pros.getProperty("password");
String url = pros.getProperty("url");
String driverClass = pros.getProperty("className");
// 3.加载驱动
Class.forName(driverClass);
// 4.获取连接
conn = DriverManager.getConnection(url, user, password);
st = conn.createStatement();
rs = st.executeQuery(sql);
// 获取结果集的元数据
ResultSetMetaData rsmd = rs.getMetaData();
// 获取结果集的列数
int columnCount = rsmd.getColumnCount();
if (rs.next()) {
t = clazz.newInstance();
for (int i = 0; i < columnCount; i++) {
// 1. 获取列的别名
String columnName = rsmd.getColumnLabel(i + 1);
// 2. 根据列名获取对应数据表中的数据
Object columnVal = rs.getObject(columnName);
// 3. 将数据表中得到的数据,封装进对象
Field field = clazz.getDeclaredField(columnName);
field.setAccessible(true);
field.set(t, columnVal);
}
return t;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (st != null) {
try {
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return null;
}
}
User:
public class User {
private String user;
private String password;
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public User(String user, String password) {
this.user = user;
this.password = password;
}
public User() {
}
@Override
public String toString() {
return "User{" +
"user='" + user + '\'' +
", password='" + password + '\'' +
'}';
}
}
SQL:
SELECT USER,PASSWORD
FROM user_table
WHERE USER = '' AND PASSWORD = '';
-- 填入用户名和密码
SELECT USER,PASSWORD
FROM user_table
WHERE USER = '1' OR ' and password = ' =1 OR '1' = '1';
- 综上,我们改用PreparedStatement来替换掉Statement来进行增改删查
PreparedStatement的使用
PreparedStatement介绍
- 可以通过调用
Connection
对象的preparedStatement(String sql)
方法获取PreparedStatement
对象 PreparedStatement
接口是Statement
的子接口,它表示一条预编译过的 SQL 语句PreparedStatement
对象所代表的 SQL 语句中的参数用问号(?
)来表示,调用PreparedStatement
对象的setXxx()
方法来设置这些参数.setXxx()
方法有两个参数,第一个参数是要设置的 SQL 语句中的参数的索引(从1开始),第二个是设置的 SQL 语句中的参数的值
PreparedStatement vs Statement
- 代码的可读性和可维护性
- PreparedStatement 能最大可能提高性能:
- DBServer会对预编译语句提供性能优化。因为预编译语句有可能被重复调用,所以语句在被DBServer的编译器编译后的执行代码被缓存下来,那么下次调用时只要是相同的预编译语句就不需要编译,只要将参数直接传入编译过的语句执行代码中就会得到执行。
- 在statement语句中,即使是相同操作但因为数据内容不一样,所以整个语句本身不能匹配,没有缓存语句的意义.事实是没有数据库会对普通语句编译后的执行代码缓存。这样每执行一次都要对传入的语句编译一次。
- PreparedStatement 可以防止 SQL 注入
Java与SQL对应数据类型转换表
Java类型 | SQL类型 |
---|---|
BOOLEAN | BIT |
BYTE | TINYINT |
short | SMALLINT |
INT | INTEGER |
LONG | BIGINT |
STRING | CHAR,VARCHAR,LONGVARCHAR |
BYTE array | BINARY , VAR BINARY |
java.sql.Date | DATE |
java.sql.Time | TIME |
java.sql.Timestamp | TIMESTAMP |
使用PreparedStatement实现增、删、改操作
- 抛异常
- try/catch/finally处理
为了更容易的看清代码结构,这里采用抛异常处理
@Test
public void testInsert() {
Connection connection = null;
PreparedStatement ps = null;
try {
//1. 读取配置文件
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");
Properties properties = new Properties();
properties.load(is);
String user = properties.getProperty("user");
String password = properties.getProperty("password");
String url = properties.getProperty("url");
String className = properties.getProperty("className");
//2. 加载驱动
Class.forName(className);
//3. 获取连接
connection = DriverManager.getConnection(url, user, password);
//4. 预编译SQL语句,返回PreparedStatement实例
String sql = "insert into customers(name,email,birth) values(?,?,?)";
ps = connection.prepareStatement(sql);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf.parse("1999-08-11");
//5. 填充占位符
ps.setObject(1,"李四");
ps.setObject(2,"[email protected]");
ps.setObject(3,date);
//6. 执行操作
ps.execute();
} catch (IOException | ClassNotFoundException | SQLException | ParseException e) {
e.printStackTrace();
} finally {
//7. 资源的关闭
if (ps!=null){
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection!=null){
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
增删改的通用操作
注意到增删改都需要建立连接和关闭资源,所以可以将这二者封装成两个方法,写成一个工具类工具类
- 工具类提供通用的连接数据库和关闭资源的操作
import java.io.InputStream; import java.sql.*; import java.util.Properties; public class JDBCUtil { public static Connection getConnection() throws Exception{ //1. 读取配置文件 InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties"); Properties properties = new Properties(); properties.load(is); String user = properties.getProperty("user"); String password = properties.getProperty("password"); String url = properties.getProperty("url"); String className = properties.getProperty("className"); //2. 加载驱动 Class.forName(className); //3. 获取连接 Connection connection = DriverManager.getConnection(url, user, password); return connection; } public static void clossResource(Connection connection, Statement ps){ //资源的关闭 if (connection!=null) { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } if (ps!=null) { try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } } }
通用的增删改操作
- 根据不同的SQL语句和占位符,可以实现任意的增删改操作,由于占位符的个数不确定,所以这里采用可变形参。
- 可变形参的底层是一个数组,所以我们可以用args.length来获取形参个数,从而进行遍历填充占位符
- 关于可变形参的说明:
- 1.可变形参的个数可以是0个,1个,任意个
- 2.可变形参的底层就是一个数组
- 3.和可变形参相同类型的数组不构成方法重载
- 4.可变形参只能放在形参列表的最后一个
- 5.在一个形参列表中最多只能有一个可变形参
public void update(String sql, Object ...args) { Connection connection = null; PreparedStatement ps = null; try { //1. 获取数据库的连接 connection = JDBCUtil.getConnection(); //2. 预编译SQL语句,返回PreparedStatement的实例 ps = connection.prepareStatement(sql); //3. 填充占位符 for (int i = 0; i < args.length; i++) { ps.setObject(i + 1, args[i]); } //4. 执行 ps.execute(); } catch (Exception e) { e.printStackTrace(); } finally { //5. 资源的关闭 JDBCUtil.clossResource(connection, ps); } }
测试
@Test public void testUpdate() throws Exception{ // 增 String sql1 = "insert into customers(name,email,birth) values(?,?,?)"; SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); java.util.Date date = sdf.parse("1997-02-27"); update(sql1,"王五","[email protected]",date); // 删 String sql2 = "delete from customers where id = ?"; update(sql2,4); // 改 String sql3 = "update `order` set order_name = ? where order_id = ?"; update(sql3,"DD",2); }
使用PreparedStatement实现查询操作
查询操作不同于增删改操作,查询操作需要我们返回一个对象
Java中万事万物皆对象,例如我们在SQL中查询到的一条Customers对象,就可以对应一个Java类的对象
其中Customers表中的每一个字段,都对应着Java类中的一个属性
针对Customers表的
不通用的
查询操作Customer类
Select操作
重载clossResource方法
下面是Customers表的描述,暂时无视掉这个photo,根据表的描述建一个Customer类
Field | Type | Null | Key | Default | Extra |
---|---|---|---|---|---|
id | int(10) | NO | PRI | auto_increment | |
name | varchar(15) | YES | |||
varchar(20) | YES | auto_increment | |||
birth | date | YES | |||
photo | mediumblob | YES |
JAVA
import java.util.Date;
public class Customer {
private int id;
private String name;
private String email;
private Date birth;
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 getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Date getBirth() {
return birth;
}
public void setBirth(Date birth) {
this.birth = birth;
}
public Customer() {
}
public Customer(int id, String name, String email, Date birth) {
this.id = id;
this.name = name;
this.email = email;
this.birth = birth;
}
@Override
public String toString() {
return "Customer{" +
"id=" + id +
", name='" + name + '\'' +
", email='" + email + '\'' +
", birth=" + birth +
'}';
}
}
针对Customers表
通用的
查询操作查询函数
测试
JAVA
public Customer customerForQuery(String sql, Object... args) {
Connection connection = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
//1. 连接数据库
connection = JDBCUtil.getConnection();
//2. 预编译SQL语句,获取PreparedStatement实例对象
ps = connection.prepareStatement(sql);
//3. 填充占位符
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
//4. 执行executeQuery(),获得结果集
rs = ps.executeQuery();
//5. 获得结果集的元数据
ResultSetMetaData rsmd = rs.getMetaData();
//6. 通过元数据获取ColumnCount,即结果集中有几个列
int columnCount = rsmd.getColumnCount();
//7. 如果有数据
if (rs.next()) {
//则生成一个对象实例,当然这里可以使用泛型
Customer customer = new Customer();
//遍历每一个列
for (int i = 0; i < columnCount; i++) {
//8. 获取列值
Object columnValue = rs.getObject(i + 1);
//9. 获取列名
String columnName = rsmd.getColumnName(i + 1);
//10. 使用反射给相应属性赋值,注意属性都是private的,将Accessible设置为true
Field field = customer.getClass().getDeclaredField(columnName);
field.setAccessible(true);
field.set(customer, columnValue);
}
return customer;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//11. 资源的关闭
JDBCUtil.clossResource(connection, ps, rs);
}
return null;
}
针对Order表
通用的
查询操作Order类
queryForOrder
测试
根据下表创建一个Order类,但SQL中的命名方式与Java中的命名方式不一样,我们按照Java中的命名方式即可,但是这样会导致反射时找不到对应的字段名去赋值。解决办法也很简单,在SQL查询语句中,可以给列起别名,将别名设定为Java中的命名方式即可,获取元数据的时候,使用getColumnLabel()方法来获取列的别名,这样就完美解决了这二者不同的命名方式产生的问题
Field | Type | Null | Key | Default | Extra |
---|---|---|---|---|---|
order_id | int(10) | NO | PRI | auto_increment | |
order_name | varchar(20) | YES | |||
order_date | date | YES |
JAVA
import java.util.Date;
public class Order {
private int orderId;
private String orderName;
private Date orderDate;
public int getorderId() {
return orderId;
}
public void setorderId(int orderId) {
this.orderId = orderId;
}
public String getorderName() {
return orderName;
}
public void setorderName(String orderName) {
this.orderName = orderName;
}
public Date getorderDate() {
return orderDate;
}
public void setorderDate(Date orderDate) {
this.orderDate = orderDate;
}
public Order(int orderId, String orderName, Date orderDate) {
this.orderId = orderId;
this.orderName = orderName;
this.orderDate = orderDate;
}
public Order() {
}
@Override
public String toString() {
return "Order{" +
"orderId=" + orderId +
", orderName='" + orderName + '\'' +
", orderDate=" + orderDate +
'}';
}
}
小结:
当表的字段名和类的属性名不同时:
- 1.在声明sql时,使用类的属性名来命名字段的别名
- 2.使用ResultSetMetaData的getColumnLabel()方法来替代getColumnName()方法,获得列的列名的别名。
- 3.说明:没有别名时,getColumnLabel()方法获取的就是列名;所以无论有没有别名,都使用getColumnLabel()方法
通用的
查询操作,但只返回一条记录通用的查询方法
测试
利用泛型方法来解决
JAVA
public <T> T getInstance(Class<T> clazz, String sql, Object... args) {
Connection connection = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
connection = JDBCUtil.getConnection();
ps = connection.prepareStatement(sql);
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
rs = ps.executeQuery();
if (rs.next()) {
T t = clazz.newInstance();
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();
for (int i = 0; i < columnCount; i++) {
Object columnValue = rs.getObject(i + 1);
String columnLabel = rsmd.getColumnLabel(i + 1);
Field field = t.getClass().getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t, columnValue);
}
return t;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtil.clossResource(connection, ps, rs);
}
return null;
}
通用的
查询操作,返回多条记录getForList
测试
只需要在返回一条记录的基础上,修改一些代码即可
JAVA
//把返回类型改为集合
public <T> ArrayList<T> getForList(Class<T> clazz, String sql, Object... args){
Connection connection = null;
PreparedStatement ps = null;
ResultSet rs = null;
ArrayList<T> ts = null;
try {
connection = JDBCUtil.getConnection();
ps = connection.prepareStatement(sql);
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
rs = ps.executeQuery();
ts = new ArrayList<>();
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();
//这里的if改成while
while (rs.next()) {
T t = clazz.newInstance();
for (int i = 0; i < columnCount; i++) {
Object columnValue = rs.getObject(i + 1);
String columnLabel = rsmd.getColumnLabel(i + 1);
Field field = t.getClass().getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t, columnValue);
}
//没查询完一条数据,将其加入到集合中
ts.add(t);
}
return ts;
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtil.clossResource(connection, ps, rs);
}
return null;
}
使用PreparedStatement代替Statement原因:
- PreparedStatement是预编译的sql语句,可以解决Statement的拼串和sql注入问题;
- PreparedStatement首先确定了语法逻辑,然后填充相应的数值;
- 而Statement会连着数值里包含的非法语法一起编译,就会造成对原来语法逻辑的破坏。
- PreparedStatement还可以操作Blob类型的数据,而Statement不行;
- PreparedStatement可以实现跟高效的批量操作:
- 如果访问10000条数据,PreparedStatement会将语法固定,只用填充占位符就好了。
JDBC API小结
- 两种思想
- 面向接口编程的思想
- ORM思想(object relational mapping)
- 一个数据表对应一个java类
- 表中的一条记录对应java类的一个对象
- 表中的一个字段对应java类的一个属性
- sql是需要结合列名和表的属性名来写。注意起别名。
- 两种技术
- JDBC结果集的元数据:ResultSetMetaData
- 获取列数:getColumnCount()
- 获取列的别名:getColumnLabel()
- 通过反射,创建指定类的对象,获取指定的属性并赋值
- JDBC结果集的元数据:ResultSetMetaData
ResultSet
- 光标默认在表头
boolean next():(1)将光标从当前位置向前移动一行(2)判断当前行是否为有效行
getXxx(参数):获取数据 参数:int:列的编号,从1开始 String:列的名称
数据库连接池
Druid
- 定义配置文件
# 简单配置
driverclassName=com.mysql.jdbc.Driver
url=jdbc:mysql:///db1?useSSL=false&useServerPrepstmts=true
username=root
password=1234
#初始化连接数员
initialsize=5
#最大连接数
maxActive=10
#最大等待时间
maxWait=3000
- 加载配置文件
Properties prop new Properties();
prop.Load(new FileInputstream(name:"src/druid.properties"));
- 获取数据库连接池对象
DataSource dataSource DruidDataSourceFactory.createDataSource(prop);
- 获取连接
Connection connection dataSource.getConnection();
System.out.println(connection);
查询、添加示例
package example;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import org.junit.Test;
import javax.sql.DataSource;
import java.io.FileInputStream;
import java.io.InputStream;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
public class brandTest {
@Test
public void selectTest() throws Exception {
// 创建资源池
Properties p = new Properties();
p.load(new FileInputStream("src/druid.properties"));
DataSource ds = DruidDataSourceFactory.createDataSource(p);
// 建立连接
Connection c = ds.getConnection();
System.out.println(c);
// 执行SQL语句
String sql = "select * from tb_brand;";
PreparedStatement pstmt = c.prepareStatement(sql);
// 获取结果 List<brand>
ResultSet rs = pstmt.executeQuery();
List<Brand> brands = new ArrayList<>();
Brand brand = null;
while(rs.next())
{
int id = rs.getInt("id");
int status = rs.getInt("status");
int ordered = rs.getInt("ordered");
String brandName = rs.getString("brand_name");
String companyName = rs.getString("company_name");
String description = rs.getString("description");
brand = new Brand();
brand.setBrandName(brandName);
brand.setCompanyName(companyName);
brand.setDescription(description);
brand.setId(id);
brand.setStatus(status);
brand.setOrdered(ordered);
brands.add(brand);
}
// 打印结果
System.out.println(brands);
rs.close();
pstmt.close();
c.close();
}
@Test
public void addTest() throws Exception {
// 创建资源池
Properties p = new Properties();
p.load(new FileInputStream("src/druid.properties"));
DataSource ds = DruidDataSourceFactory.createDataSource(p);
// 建立连接
Connection c = ds.getConnection();
System.out.println(c);
// 执行SQL语句
String sql = "insert into tb_brand() values(?, ?, ?, ?, ?, ?);";
PreparedStatement pstmt = c.prepareStatement(sql);
// 添加参数
pstmt.setInt(1, 1);
pstmt.setString(2,"wjm");
pstmt.setString(3,"mtmn");
pstmt.setInt(4,66);
pstmt.setString(5,"😄");
pstmt.setInt(6,0);
// 获取结果
Integer rs = pstmt.executeUpdate();
// 打印结果
if(rs > 0) {System.out.println("add successful!");}
else {System.out.println("add failed!");}
pstmt.close();
c.close();
}
}
操作BLOB类型字段
MySQL BLOB类型
- MySQL中,BLOB是一个二进制大型对象,是一个可以存储大量数据的容器,它能容纳不同大小的数据。
- 插入BLOB类型的数据必须使用PreparedStatement,因为BLOB类型的数据无法使用字符串拼接写的。
- MySQL的四种BLOB类型(除了在存储的最大信息量上不同外,他们是等同的)
类型 | 大小{单位:字节) |
---|---|
TINYBLOB | 最大255 |
BLOB | 最大65K |
MEDIUMBLOB | 最大16M |
LONGBLOB | 最大4G |
- 实际使用中根据需要存入的数据大小定义不同的BLOB类型。
- 需要注意的是:如果存储的文件过大,数据库的性能会下降。
- 如果在指定了相关的Blob类型以后,还报错:xxx too large,那么在mysql的安装目录下,找my.ini文件加上如下的配置参数: max_allowed_packet=16M。同时注意:修改了my.ini文件之后,需要重新启动mysql服务。
向数据表中插入大数据类型
@Test
public void blobTest() {
Connection connection = null;
PreparedStatement ps = null;
try {
String sql = "insert into customers(name,email,birth,photo) values(?,?,?,?)";
//获取连接
connection = JDBCUtil.getConnection();
//预编译SQL语句,获取prepareStatement实例对象
ps = connection.prepareStatement(sql);
//填充占位符
ps.setObject(1, "Kyle");
ps.setObject(2, "[email protected]");
ps.setObject(3, "1997-08-07");
//操作Blob类型变量
ps.setBlob(4, new FileInputStream("头像.png"));
//执行
ps.execute();
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtil.clossResource(connection, ps);
}
}
如果在指定了相关的Blob类型以后,还报错∶xxx too large,那么在mysql的安装目录下,找my.ini文件加上如下的配置参数: max_allowed_packet=16M。同时注意∶修改了my.ini文件之后,需要重新启动mysql服务。
从数据表中读取大数据类型
@Test
public void blobTest2() {
Connection connection = null;
PreparedStatement ps = null;
ResultSet rs = null;
InputStream is = null;
OutputStream os = null;
try {
String sql = "select id,name,email,birth,photo from customers where id = ?";
connection = JDBCUtil.getConnection();
ps = connection.prepareStatement(sql);
ps.setObject(1, 16);
rs = ps.executeQuery();
if (rs.next()){
int id = rs.getInt("id");
String name = rs.getString("name");
String email = rs.getString("email");
Date birth = rs.getDate("birth");
Customer customer = new Customer(id, name, email, birth);
System.out.println(customer);
Blob photo = rs.getBlob("photo");
is = photo.getBinaryStream();
os = new FileOutputStream("照片.png");
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
JDBCUtil.clossResource(connection, ps, rs);
}
}
批量插入
批量执行SQL语句
- 当需要成批插入或者更新记录时,可以采用Java的批量
更新
机制,这一机制允许多条语句一次性提交给数据库批量处理。通常情况下比单独提交处理更有效率 - JDBC的批量处理语句包括下面三个方法:
- addBatch(String):添加需要批量处理的SQL语句或是参数;
- executeBatch():执行批量处理语句;
- clearBatch():清空缓存的数据
- 通常我们会遇到两种批量执行SQL语句的情况:
- 多条SQL语句的批量处理;
- 一个SQL语句的批量传参;
高效的批量插入
举例:向数据表中插入20000条数据
- 首先先创建一个goods表
CREATE TABLE goods( id INT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(25) );
下面是四种实现方式,效率逐渐提高,都是基于低层次实现的优化
🚩实现一
使用Statement
//耗时:15006ms
@Test
public void testInsert01() {
Connection connection = null;
Statement st = null;
try {
connection = JDBCUtil.getConnection();
st = connection.createStatement();
long start = System.currentTimeMillis();
for (int i = 1; i <= 20000; i++) {
String sql = "insert into goods(name) values('name_ " + i + "')";
st.executeUpdate(sql);
}
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start) + "ms");
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtil.clossResource(connection, st);
}
}
🚩实现二
- 相较于实现层次一的优点
- 即PreparedStatement优于Statement的地方:
- 在于sql语句Statement内存中会有很多个sql语句,并且每次都会做一次语法检查,而PreparedStatement只有一个sql语句,每次只是填充占位符。
//耗时:14682ms
@Test
public void testInsert02(){
Connection connection = null;
PreparedStatement ps = null;
try {
String sql = "insert into goods(name) values(?)";
connection = JDBCUtil.getConnection();
ps = connection.prepareStatement(sql);
long start = System.currentTimeMillis();
for (int i = 0; i < 20000; i++) {
ps.setObject(1, "name_" + i);
}
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start) + "ms");
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtil.clossResource(connection, ps);
}
}
🚩实现三
- 相较于实现层次二的优点
- 使用Batch批量处理:addBatch()、executeBatch()、clearBatch()
- mysql服务器默认是关闭批处理的,我们需要通过一个参数,让mysql开启批处理的支持。在jdbc.properties配置文件的url后添上:?rewriteBatchedStatements=true
//耗时:196ms
@Test
public void testInsert03() {
Connection connection = null;
PreparedStatement ps = null;
try {
String sql = "insert into goods(name) values(?)";
connection = JDBCUtil.getConnection();
ps = connection.prepareStatement(sql);
long start = System.currentTimeMillis();
for (int i = 0; i < 20000; i++) {
ps.setObject(1, "name_" + i);
//1.“攒”sql
ps.addBatch();
if (i % 10000 == 0){
//2.执行
ps.executeBatch();
//3.清空
ps.clearBatch();
}
}
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start) + "ms");
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtil.clossResource(connection, ps);
}
}
🚩实现四
再快点
- 每500条数据执行一次ps.executeBatch();
- 这样每500条就会提交一次。
- 但每次提交都会占用一点时间,所以先不提交,都传完以后,最后再提交。
//耗时:157ms
@Test
public void testInsert04() {
Connection connection = null;
PreparedStatement ps = null;
try {
String sql = "insert into goods(name) values(?)";
connection = JDBCUtil.getConnection();
//1.设置为不自动提交数据
connection.setAutoCommit(false);
ps = connection.prepareStatement(sql);
long start = System.currentTimeMillis();
for (int i = 0; i < 20000; i++) {
ps.setObject(1, "name_" + i);
//1.“攒”sql
ps.addBatch();
if (i % 10000 == 0){
//2.执行
ps.executeBatch();
//3.清空
ps.clearBatch();
}
//2.提交数据
connection.commit();
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start) + "ms");
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtil.clossResource(connection, ps);
}
}
数据库事务
Maven
JavaWeb---Maven&MyBatis | Kyle's Blog (cyborg2077.github.io)
- 提供了一套标准化的项目结构
- 提供了一套标准化的构建流程(编译,测试,打包,发布.…)
- 提供了一套依赖管理机制
安装,改目录,改配置:本地仓库、私有仓库、阿里源
MyBatis
JavaWeb---Maven&MyBatis | Kyle's Blog (cyborg2077.github.io)
持久层框架,用于简化JDBC
持久层:负责将数据到保存到数据库的那一层代码
JDBC缺点:
- 硬编码:ip、用户名、密码、SQL语句等写在代码里
- 操作繁琐:手动设置参数、封装结果
一些Maven id
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.5</version>
</dependency>
<!-- 添加slf4j日志api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.20</version>
</dependency>
<!-- 添加logback-classic依赖 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<!-- 添加logback-core依赖 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency><dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.5</version>
</dependency>
logback配置文件
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--
CONSOLE :表示当前的日志信息是可以输出到控制台的。
-->
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[%level] %cyan([%thread]) %boldGreen(%logger{15}) - %msg %n</pattern>
</encoder>
</appender>
<logger name="com.itheima" level="DEBUG" additivity="false">
<appender-ref ref="Console"/>
</logger>
<!--
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF
, 默认debug
<root>可以包含零个或多个<appender-ref>元素,标识这个输出位置将会被本日志级别控制。
-->
<root level="DEBUG">
<appender-ref ref="Console"/>
</root>
</configuration>
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<!--数据库的连接信息-->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///tb_brand?useSSL=false"/>
<property name="username" value="cdd"/>
<property name="password" value="1"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--加载SQL映射文件-->
<mapper resource="BrandMapper.xml"/>
</mappers>
</configuration>
SQL映射文件 表名Mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.example.BlogMapper">
<!--id是整个sql语句的唯一标识,resultType是结果的类型-->
<select id="selectAll" resultType="example.Brand">
select * from tb_brand;
</select>
</mapper>
入门示例
package example;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import java.io.IOException;
import java.util.List;
public class MyBatisTest {
@Test
public void mian() throws Exception {
// 加载mybatis配置文件, 获取SqlSessionFactory
String resource = "mybatis-config.xml";
SqlSessionFactory sqlSF = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream(resource));
// 获取SqlSession对象, 用来执行sql
SqlSession sqlSession = sqlSF.openSession();
// 执行sql
List<Brand> brands = sqlSession.selectList("test.selectAll");
System.out.println(brands);
// 释放资源
sqlSession.close();
}
}
Mapper代理开发
- 解决test.selectAll的硬编码问题
List<User> users = sqlSession.selectList("UserMapper.selectAll");
System.out.println(users);
- 先创建一个UserMapper类对象,调用对象的方法
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> users = userMapper.selectAll();
- 定义与SQL映射文件同名的Mapper接口,并且将Mapper接口和SQL映射文件放置在同一目录下

- 设置SQL映射文件的namespace属性为Mapper接口全限定名
<mapper namespace="com.blog.mapper.UserMapper">
- 在 Mapper 接口中定义方法,方法名就是SQL映射文件中sql语句的id,并保持参数类型和返回值类型一致
<select id="selectAll" resultType="com.blog.pojo.User">
import com.blog.pojo.User;
import java.util.List;
public interface UserMapper {
List<User> selectAll();
}
示例
- 定义与SQL映射文件同名的Mapper接口,并且将Mapper接口和SQL映射文件放置在同一目录下

- 设置SQL映射文件的namespace属性为Mapper接口全限定名


package example;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import java.io.IOException;
import java.util.List;
public class MyBatisTest {
@Test // Mapper代理
public void main() throws Exception
{
// 加载mybatis配置文件, 获取SqlSessionFactory
String resource = "mybatis-config.xml";
SqlSessionFactory sqlSF = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream(resource));
// 获取SqlSession对象, 用来执行sql
SqlSession sqlSession = sqlSF.openSession();
// 执行sql
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
List<Brand> brands = brandMapper.selectAll();
System.out.println(brands);
// 释放资源
sqlSession.close();
}
}
包扫描
<mappers>
<!--如果要在这里写好多映射文件,一个一个手写,也太麻烦了,如果你遵循了Mapper代理方式,就可以用包扫描的方式来简化操作-->
<!--<mapper resource="example/BrandMapper.xml"/>-->
<package name="example"/>
</mappers>
MyBatis核心文件配置
MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。 配置文档的顶层结构如下:
- configuration(配置)
- properties(属性)
- settings(设置)
- typeAliases(类型别名)
- typeHandlers(类型处理器)
- objectFactory(对象工厂)
- plugins(插件)
- environments(环境配置)
- environment(环境变量)
- transactionManager(事务管理器)
- dataSource(数据源)
- environment(环境变量)
- databaseIdProvider(数据库厂商标识)
- mappers(映射器)
配置各个标签的时候,必须遵守前后顺序
类型别名
<typeAliases>
<!--name属性的值是实体类所在包-->
<package name="com.blog.pojo"/>
</typeAliases>
案例
执行测试方法部分结果如下Brand{id=1, brandName='null', companyName='null', ordered=5, description='好吃不上火', status=0
其中brandName和companyName的值为NULL,究其原因是我们Brand类中的属性名和MySQL中的列名不一致。
这个问题我在上篇JDBC文章中给出了一个比较初级的解决方案,那就是给列取别名,令别名与我们Java中的命名方式相同,这样我们就可以通过获取列的别名,来进行反射赋值。
另一种解决方式是使用resultMap定义字段和属性的映射关系(推荐)
起别名解决上述问题
我们可以在写sql语句时给这两个字段起别名,将别名定义成和属性名一致即可。
XML
<select id="selectAll" resultType="brand">
select
id, brand_name as brandName, company_name as companyName, ordered, description, status
from tb_brand;
</select>
而上面的SQL语句中的字段列表书写麻烦,如果表中还有更多的字段,同时其他的功能也需要查询这些字段时就显得我们的代码不够精炼。Mybatis提供了sql
片段可以提高sql的复用性。
resultMap做映射
<resultMap id="brandResultMap" type="example.Brand">
<!--
id:完成主键字段的映射
column:表的列名
property:实体类的属性名
result:完成一般字段的映射
column:表的列名
property:实体类的属性名
-->
<result column="brand_name" property="brandName"/>
<result column="company_name" property="companyName"/>
</resultMap>
<!--id是整个sql语句的唯一标识,resultType是结果的类型-->
<!-- <select id="selectAll" resultType="example.Brand">
select * from tb_brand;
</select>-->
<select id="selectAll" 🚩resultMap="brandResultMap">
select *
from tb_brand;
</select>
<select id="selectByID" resultType="example.Brand"></select>
传参数
- 参数占位符
- #{} 替换为? 防止SQL注入
- ${} 直接拼参数,有SQL注入问题
<select id="selectByID" resultMap="brandResultMap">
select * from tb_brand where id = 🚩#{id};
</select>
SQL语句中特殊字段处理
在xml中,”<”、”>”、”&”等字符是不能直接存入的,否则xml语法检查时会报错,如果想在xml中使用这些符号,必须将其转义为实体,如<
、>
、&
,这样才能保存进xml文档。或者使用<![CDATA[]]>
,被这个标记所包含的内容将表示为纯文本
但是严格来说,在XML中只有”<”和”&”是非法的,其它三个都是可以合法存在的,但是,把它们都进行转义是一个好的习惯。
不管怎么样,转义前的字符也好,转义后的字符也好,都会被xml解析器解析,为了方便起见,使用<![CDATA[]]>
来包含不被xml解析器解析的内容。但要注意的是:
- 此部分不能再包含
]]>
; - 不允许嵌套使用;
]]>
这部分不能包含空格或者换行。
最后,说说<![CDATA[]]>
和xml转移字符的关系,它们两个看起来是不是感觉功能重复了?
- 是的,它们的功能就是一样的,只是应用场景和需求有些不同:
<![CDATA[]]>
不能适用所有情况,转义字符可以;- 对于短字符串
<![CDATA[]]>
写起来啰嗦,对于长字符串转义字符写起来可读性差; <![CDATA[]]>
表示xml解析器忽略解析,所以更快。
传递多个参数
在 BrandMapper
接口中定义多条件查询的方法。
🚩散装
@Test
public void testSelectByCondition() throws IOException {
// 接收参数
int status = 1;
String companyName = "华为";
String brandName = "华为";
// 处理参数
companyName = "%" + companyName + "%";
brandName = "%" + brandName + "%";
//1. 加载MyBatis核心配置文件,获取SqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
List<Brand> brands = brandMapper.selectByCondition(status, companyName, brandName);
System.out.println(brands);
//5. 资源关闭
sqlSession.close();
}
🚩封装成对象
@Test
public void testSelectByCondition() throws IOException {
// 接收参数
int status = 1;
String companyName = "华为";
String brandName = "华为";
// 处理参数
companyName = "%" + companyName + "%";
brandName = "%" + brandName + "%";
// 创建对象
Brand brand = new Brand();
brand.setStatus(status);
brand.setCompanyName(companyName);
brand.setBrandName(brandName);
//1. 加载MyBatis核心配置文件,获取SqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
List<Brand> brands = brandMapper.selectByCondition(brand);
System.out.println(brands);
//5. 资源关闭
sqlSession.close();
}
🚩封装到map集合
@Test
public void testSelectByCondition() throws IOException {
// 接收参数
int status = 1;
String companyName = "华为";
String brandName = "华为";
// 处理参数
companyName = "%" + companyName + "%";
brandName = "%" + brandName + "%";
// 封装map
HashMap map = new HashMap();
map.put("status", status);
map.put("companyName", companyName);
map.put("brandName", brandName);
//1. 加载MyBatis核心配置文件,获取SqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
List<Brand> brands = brandMapper.selectByCondition(map);
System.out.println(brands);
//5. 资源关闭
sqlSession.close();
}
🚩上述方法的缺点:当用户没有输入全部参数,会查询不到结果 ---> 使用动态SQL
动态条件查询
- if
<select id="selectByCondition" resultMap="brandResultMapper">
select *
from tb_brand
where
<if test="status != null">
`status` = #{status}
</if>
<if test="companyName != null and companyName != ''">
and company_name like #{companyName}
</if>
<if test="brandName != null and brandName != ''">
and brand_name like #{brandName};
</if>
</select>
如上的这种SQL语句就会根据传递的参数值进行动态的拼接。如果此时status和companyName有值那么就会值拼接这两个条件。SQL语句将变成
select * from tb_brand where status = ? and company_name like ?
但如果我们只给companyName这一个参数,那么SQL语句会变成下面这样
select * from tb_brand where and company_name like ?
WHERE关键字后面直接跟了个AND,变成了一条错误的SQL语句,那么最笨的一个解决方案就是在where后面先接一个恒等式
select * from tb_brand where 1 = 1 and company_name like ?
但MyBatis也料想到了这种情况,所以MyBatis又提供了一个where标签
- where
<select id="selectByCondition" resultMap="brandResultMapper">
select *
from tb_brand
<where>
<if test="status != null">
`status` = #{status}
</if>
<if test="companyName != null and companyName != ''">
and company_name like #{companyName}
</if>
<if test="brandName != null and brandName != ''">
and brand_name like #{brandName};
</if>
</where>
</select>
单个条件的动态SQL

- choose
- 在 BrandMapper.xml 映射配置文件中编写 statement,使用 resultMap 替换 resultType
将where替换成<where>
标签,这样当我们没有选中任何查询方式时,会自动帮我们去掉where,从而查询所有数据
或者保持where不变,在choose中添加<otherwise>
标签,在其中写入一个恒等式,这样当没有选中任何查询方式时, - SQL语句会变成select * from tb_brand where true,同样实现查询所有数据的效果,但还是推荐前者的方式
<select id="selectByConditionSingle" resultType="com.blog.pojo.Brand">
select *
from tb_brand
<where>
<choose>
<when test="status != null">
`status` = #{status}
</when>
<when test="companyName != null and companyName != ''">
companyName = #{companyName}
</when>
<when test="brandName != null and brandName != ''">
brandName = #{brandName}
</when>
</choose>
</where>
</select>
添加
在第2步获取SqlSession对象时,默认是不会自动提交事务的,我们可以在openSession方法中加上true,这样就能自动提交事务了,不用手动调用commit方法
SqlSession sqlSession = sqlSessionFactory.openSession(true); //设置自动提交事务,这种情况不需要手动提交事务了
@Test
public void testAdd() throws IOException {
//接收参数
String brandName = "波导";
String companyName = "波导手机";
Integer ordered = 100;
String description = "手机中的战斗机";
int status = 1;
//封装对象
Brand brand = new Brand();
brand.setBrandName(brandName);
brand.setCompanyName(companyName);
brand.setOrdered(ordered);
brand.setDescription(description);
brand.setStatus(status);
//1. 获取SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
brandMapper.add(brand);
//提交事务
sqlSession.commit();
//5. 释放资源
sqlSession.close();
}
主键返回
在接收参数的时候,我们没有接收id的参数,而是利用数据库主键自增长来自动赋值,但有时候我们又需要获取这个自增长的id
解决方案:
在 insert 标签上添加如下属性:
- useGeneratedKeys:是够获取自动增长的主键值。true表示获取
- keyProperty :指定将获取到的主键值封装到哪儿个属性里
<insert id="add" useGeneratedKeys="true" keyProperty="id">
insert into tb_brand(brand_name, company_name, ordered, description, status)
VALUES (#{brandName},#{companyName},#{ordered},#{description},#{status})
</insert>
修改
编写接口方法
在 BrandMapper
接口中定义修改方法。int获取修改的行数
int update(Brand brand);
编写SQL语句
在 BrandMapper.xml
映射配置文件中编写修改数据的 statement
。
<update id="update">
update tb_brand
set brand_name = #{brandName},
company_name = #{companyName},
ordered = #{ordered},
`description` = #{description},
`status` = #{status}
where id = #{id}
</update>
编写测试方法
在 test/java
下的 com.blog.mapper
包下的 MybatisTest类中
定义测试方法
@Test
public void testUpdate() throws IOException {
//接收参数
int id = 5;
String brandName = "波导";
String companyName = "波导手机";
Integer ordered = 200;
String description = "波导手机,手机中的战斗机";
int status = 5;
//封装对象
Brand brand = new Brand();
brand.setBrandName(brandName);
brand.setCompanyName(companyName);
brand.setOrdered(ordered);
brand.setDescription(description);
brand.setStatus(status);
brand.setId(id);
//1. 获取SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象,并设置自动提交事务
SqlSession sqlSession = sqlSessionFactory.openSession(true);
//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
int updateCount = brandMapper.update(brand);
System.out.println(updateCount);
//5. 释放资源
sqlSession.close();
}
此种修改方式要改只能全部改,如果没有给某一个字段赋值,那么修改之后的值就是null,十分的不方便,所以我们要将其优化成动态的修改字段
修改动态字段
解决方案跟上面的类似,也是用if标签来判断用户的输入,然后用set标签来删除额外的逗号(上面是用where标签去除and或or),防止出现SQL语法错误
<update id="update">
update tb_brand
<set>
<if test="brandName != null and brandName != ''">
brand_name = #{brandName},
</if>
<if test="companyName != null and companyName != ''">
company_name = #{companyName},
</if>
<if test="ordered != null">
ordered = #{ordered},
</if>
<if test="description != null and description != ''">
`description` = #{description},
</if>
<if test="status != null">
`status` = #{status}
</if>
</set>
where id = #{id}
</update>
删除一行数据
我们在App网购的时候,购物车里都会有删除
按钮,当用户点击了该按钮,就会将改行数据删除掉。那我们就需要思考,这种删除是根据什么进行删除呢?
是通过主键id删除,因为id是表中数据的唯一标识。
接下来就来实现该功能。
编写接口方法
在 BrandMapper
接口中定义根据id删除方法。
void deleteById(int id);
编写SQL语句
在 BrandMapper.xml
映射配置文件中编写删除一行数据的 statement
<delete id="deleteById">
delete
from tb_brand
where id = #{id};
</delete>
编写测试方法
在 test/java
下的 com.blog.mapper
包下的 MybatisTest类中
定义测试方法
@Test
public void testDeleteById() throws IOException {
//接收参数
int id = 6;
//1. 获取SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession(true);
//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
brandMapper.deleteById(id);
//5. 释放资源
sqlSession.close();
}
批量删除
我们在删除购物车订单的时候,都会有个多选按钮,可以选中多条记录进行删除,下面我们来实现这个功能
编写接口方法
在 BrandMapper
接口中定义删除多行数据的方法。
// 参数是一个数组,数组中存储的是多条数据的id
void deleteByIds(int[] ids);
编写SQL语句
在 BrandMapper.xml
映射配置文件中编写删除多条数据的 statement
。
编写SQL时需要遍历数组来拼接SQL语句。Mybatis 提供了 foreach
标签供我们使用
foreach 标签
用来迭代任何可迭代的对象(如数组,集合)。
- collection 属性:
- mybatis会将数组参数,封装为一个Map集合。
- 默认:array = 数组
- 使用@Param注解改变map集合的默认key的名称
- mybatis会将数组参数,封装为一个Map集合。
- item 属性:本次迭代获取到的元素。
- separator 属性:集合项迭代之间的分隔符。
foreach
标签不会错误地添加多余的分隔符。也就是最后一次迭代不会加分隔符。 - open 属性:该属性值是在拼接SQL语句之前拼接的语句,只会拼接一次
- close 属性:该属性值是在拼接SQL语句拼接后拼接的语句,只会拼接一次
<delete id="deleteByIds">
delete from tb_brand
where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
假如数组中的id数据是{1,2,3},那么拼接后的sql语 句就是:delete from tb_brand where id in (1,2,3);
编写测试方法
在 test/java
下的 com.blog.mapper
包下的 MybatisTest类中
定义测试方法
@Test
public void testDeleteByIds() throws IOException {
//接收参数
int[] ids = {1,2,3};
//1. 获取SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession(true);
//3. 获取Mapper接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
brandMapper.deleteByIds(ids);
//5. 释放资源
sqlSession.close();
}
MyBatis参数传递
Mybatis 接口方法中可以接收各种各样的参数,如下:
- 多个参数
- 单个参数:单个参数又可以是如下类型
- POJO 类型
- Map 集合类型
- Collection 集合类型
- List 集合类型
- Array 类型
- 其他类型
多个参数
如下面的代码,就是接收两个参数,而接收多个参数需要使用 @Param
注解,那么为什么要加该注解呢?这个问题要弄明白就必须来研究Mybatis的底层对于这些参数是如何处理的。
User select(@Param("username") String username,@Param("password") String password);
<select id="select" resultType="user">
select *
from tb_user
where
username=#{username}
and password=#{password}
</select>
我们在接口方法中定义多个参数,Mybatis 会将这些参数封装成 Map 集合对象,值就是参数值,而键在没有使用 @Param
注解时有以下命名规则:
- 以 arg 开头 :第一个参数就叫 arg0,第二个参数就叫 arg1,以此类推。如:
- map.put(“arg0”,参数值1);
- map.put(“arg1”,参数值2);
- 以 param 开头 : 第一个参数就叫 param1,第二个参数就叫 param2,依次类推。如:
- map.put(“param1”,参数值1);
- map.put(“param2”,参数值2);
下面我们来验证一下
- 在
UserMapper
接口中定义如下方法
User select(String username,String password);
- 在
UserMapper.xml
映射配置文件中定义SQL
<select id="select" resultType="user">
select *
from tb_user
where
username=#{arg0} <!--username=#{param1}-->
and password=#{arg1} <!--and password=#{param2}-->
</select>
运行代码结果如下
[DEBU6][main] c.i.m.0.select- ==> Preparing: SELECT * FROM tb_user WHERE username = ? AND PASSWORD = ?
[DEBU6] [main] c.i.m.U.select- ==> Parameters: zhangsan(STRING),123(STRING)
在映射配合文件的SQL语句中使用用 arg
开头的和 param
书写,代码的可读性会变的特别差,此时可以使用 @Param
注解。
在接口方法参数上使用 @Param
注解,Mybatis 会将 arg
开头的键名替换为对应注解的属性值。
以后接口参数是多个时,在每个参数上都使用 @Param
注解。这样代码的可读性更高。
单个参数
POJO 类型
直接使用。要求
属性名
和参数占位符名称
一致Map 集合类型
直接使用。要求
map集合的键名
和参数占位符名称
一致Collection 集合类型
Mybatis 会将集合封装到 map 集合中,如下:
map.put(“arg0”,collection集合);
map.put(“collection”,collection集合;
可以使用
@Param
注解替换map集合中默认的 arg 键名。List 集合类型
Mybatis 会将集合封装到 map 集合中,如下:
map.put(“arg0”,list集合);
map.put(“collection”,list集合);
map.put(“list”,list集合);
可以使用
@Param
注解替换map集合中默认的 arg 键名。Array 类型
Mybatis 会将集合封装到 map 集合中,如下:
map.put(“arg0”,数组);
map.put(“array”,数组);
可以使用
@Param
注解替换map集合中默认的 arg 键名。其他类型
比如int类型,
参数占位符名称
叫什么都可以。尽量做到见名知意
注解实现CURD
使用注解开发会比配置文件开发更加方便。如下就是使用注解进行开发
@Select(value = "select * from tb_user where id = #{id}")
public User select(int id);
注解是用来替换映射配置文件方式配置的,所以使用了注解,就不需要再映射配置文件中书写对应的 statement
Mybatis 针对 CURD 操作都提供了对应的注解,已经做到见名知意。如下:
- 查询 :@Select
- 添加 :@Insert
- 修改 :@Update
- 删除 :@Delete
接下来我们做一个案例来使用 Mybatis 的注解开发
代码实现:
- 将之前案例中
UserMapper.xml
中的 根据id查询数据 的statement
删掉 - 在
UserMapper
接口的selectById
方法上添加注解
@Select("select * from tb_user where id = #{id}") User selectById(int id);
- 测试
@Test public void testSelect() throws IOException { //接收参数 int id = 2; //1. 获取SqlSessionFactory String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //2. 获取SqlSession对象 SqlSession sqlSession = sqlSessionFactory.openSession(true); //3. 获取Mapper接口的代理对象 UserMapper userMapper = sqlSession.getMapper(UserMapper.class); //4. 执行方法 User user = userMapper.selectById(id); System.out.println(user); //5. 释放资源 sqlSession.close(); }
- 将之前案例中
注意在官方文档中 入门
中有这样的一段话:
使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。
结论:注解完成简单功能,配置文件完成复杂功能。
HTML (HyperText Markup Language)
- HTML标签不像XML那样可以自定义,HTML中的标签都是预定义好的,运行在浏览器上并由浏览器解析,然后展示出对应的效果。
W3C标准:
- W3C是万维网联盟,这个组成是用来定义标准的。他们规定了一个网页是由三部分组成,分别是:
- 结构:对应的是 HTML 语言
- 表现:对应的是 CSS 语言
- 行为:对应的是 JavaScript 语言
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>
<!--HTML5的标识-->
<!DOCTYPE html>
<html lang="en">
<head>
<!--设定字符集-->
<meta charset="UTF-8">
<title>我的第一个网页</title>
</head>
<body>
<font color="red" size="5" face="楷体">HELLO WORLD!!</font>
</body>
</html>
注意:font 标签已经不建议使用了,以后如果要改变文字字体,大小,颜色可以使用 CSS 进行设置。
HTML 标签不区分大小写
HTML 标签属性值 单双引皆可
如上案例中的color属性值使用双引号也是可以的。
<font color="red"></font>
标签 | 描述 |
---|---|
<HTML> | 定义HTML文档 |
<head> | 定义关于文档的信息 |
<title> | 定义文档的标题 |
<body> | 定义文档的主体 |
基础标签
基础标签就是一些和文字相关的标签,如下:
标签 | 描述 |
---|---|
<h1> ~ <h6> | 定义标题,h1最大,h6最小 |
<font> | 定义文本的字体、字体尺寸、字体颜色 |
<b> | 定义粗体文本 |
<i> | 定义斜体文本 |
<u> | 定义文本下划线 |
<center> | 定义文本居中 |
<p> | 定义段落 |
<br> | 定义折行 |
<hr> | 定义水平线 |
- 注意,HTML 中的预留字符必须被替换为字符实体。 HTML 实体 在HTML 中,某些字符是预留的。 在HTML 中不能使用小于号(<)和大于号(>),这是因为浏览器会误认为它们是标签。如果需要一些特殊字符,需要借助HTML实体来实现。
HTML ISO-8859-1 参考手册 (w3school.com.cn)
图片、音频、视频标签
标签 | 描述 |
---|---|
<img> | 定义图片 |
<audio> | 定义音频 |
<video> | 定义视频 |
- img:定义图片
- src:规定显示图像的 URL(统一资源定位符)
- height:定义图像的高度
- width:定义图像的宽度
- audio:定义音频。支持的音频格式:MP3、WAV、OGG
- src:规定音频的 URL
- controls:显示播放控件
- video:定义视频。支持的音频格式:MP4, WebM、OGG
- src:规定视频的 URL
- controls:显示播放控件
尺寸单位:
height属性和width属性有两种设置方式:
- 像素:单位是px
- 百分比。占父标签的百分比。例如宽度设置为 50%,意思就是占它的父标签宽度的一般(50%)
超链接标签
在网页中可以看到很多超链接标签,当我们点击这些超链接时会跳转到其他的页面或者资源
a
标签属性:
- href:指定访问资源的URL
- target:指定打开资源的方式
- _self:默认值,在当前页面打开
- _blank:在空白页面打开
列表标签
HTML 中列表分为
有序列表
- 咖啡
- 牛奶
- 可乐
无序列表
- Apex
- Deadcells
- Terraria
标签说明:
标签 | 描述 |
---|---|
<ol> | 定义有序列表 |
<ul> | 定义无序列表 |
<li> | 定义列表项 |
有序列表中的 type
属性用来指定标记的标号的类型(数字、字母、罗马数字等)
无序列表中的 type
属性用来指定标记的形状
表格标签
- table :定义表格
- border:规定表格边框的宽度
- width :规定表格的宽度
- cellspacing:规定单元格之间的空白
- tr :定义行
- align:定义表格行的内容对齐方式
- td :定义单元格
- rowspan:规定单元格可横跨的行数(横向合并单元格)
- colspan:规定单元格可横跨的列数(纵向合并单元格)
- th:定义表头单元格
布局标签
标签 | 描述 |
---|---|
<div> | 定义HTML文档中的一个区域部分,经常与CSS一起使用,用来布局网页 |
<span> | 用于组合行内元素。 |
这两个标签,一般都是和css结合到一块使用来实现页面的布局。
div
标签 在浏览器上会有换行的效果,而 span
标签在浏览器上没有换行效果。
表单标签
表单标签效果大家其实都不陌生,像登陆页面、注册页面等都是表单。
表单就是用来采集用户输入的数据,然后将数据发送到服务端,服务端会对数据库进行操作,比如注册就是将数据保存到数据库中,而登陆就是根据用户名和密码进行数据库的查询操作。
表单标签概述
表单:在网页中主要负责数据采集功能,使用<form>
标签定义表单
表单项(元素):不同类型的 input 元素、下拉列表、文本域等
标签 | 描述 |
---|---|
<form> | 定义表单 |
<input> | 定义表单项,通过type属性控制输入形式 |
<label> | 为表单项定义标注 |
<select> | 定义下拉列表 |
<option> | 定义下拉列表的列表项 |
<textarea> | 定义文本域 |
form
是表单标签,它在页面上没有任何展示的效果。需要借助于表单项标签来展示不同的效果。
form标签属性
action:规定当提交表单时向何处发送表单数据,该属性值就是URL
以后会将数据提交到服务端,该属性需要书写服务端的URL。而今天我们可以书写
#
,表示提交到当前页面来看效果method :规定用于发送表单数据的方式
method取值有如下两种:
- get:默认值。如果不设置method属性则默认就是该值
- 请求参数会拼接在URL后边
- url的长度有限制 4KB
- post:
- 浏览器会将数据放到http请求消息体中
- 请求参数无限制的
- get:默认值。如果不设置method属性则默认就是该值
代码演示
由于表单标签在页面上没有任何展示的效果,所以在演示的过程是会先使用 input
这个表单项标签展示输入框效果
代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form>
<input type="text">
<input type="submit">
</form>
</body>
</html>
效果展示如下
从效果可以看到页面有一个输入框,用户可以在数据框中输入自己想输入的内容,点击提交按钮以后会将数据发送到服务端,当然现在肯定不能实现。现在我们可以将 form
标签的 action
属性值设置为 #
,将其将数据提交到当前页面。还需要注意一点,要想提交数据,input
输入框必须设置 name
属性。代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="#">
用户名<input type="text" name="username"><br>
密码<input type="password" name="password"><br>
<input type="submit">
</form>
</body>
</html>
接下来我们来聊 method
属性,默认是 method = 'get'
,所以该取值就会将数据拼接到URL的后面。输入用户名为ASD
,密码为ASDEQW
,点击提交,浏览器的效果如下:http://localhost:63342/JavaWeb/html-demo/html/01-%E5%9F%BA%E7%A1%80%E6%A0%87%E7%AD%BE.html?username=ASD&password=ASDEQW#
表单项标签
表单项标签有很多,不同的表单项标签有不同的展示效果。表单项标签可以分为以下三个:
<input>
:表单项,通过type属性控制输入形式input
标签有个type
属性。type
属性的取值不同,展示的效果也不一样
type取值 | 描述 |
---|---|
text | ![]() |
password | ![]() |
radio | ![]() |
checkbox | ![]() |
file | ![]() |
hidden | 定义隐藏的输入字段 |
submit | ![]() |
reset | ![]() |
button | ![]() |
<select>
:定义下拉列表,<option>
定义列表项<textarea>
:文本域,它可以输入多行文本,而input
数据框只能输入一行文本。

以上标签项的内容要想提交,必须得定义
name
属性。每一个标签都有id属性,id属性值是唯一的标识。
单选框、复选框、下拉列表需要使用
value
属性指定提交的值。练习:实现以下表单样式
代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="#">
用户名:
<input type="text" name="username"><br>
密码:
<input type="password" name="password"><br>
性别:
<input type="radio" name="gender" value="1">男
<input type="radio" name="gender" value="2">女<br>
爱好:
<input type="checkbox" name="hobbby" value="1">Apex
<input type="checkbox" name="hobbby" value="2">Deadcells
<input type="checkbox" name="hobbby" value="3">Terraria<br>
头像:
<input type="file"><br>
城市:
<select>
<option>北京</option>
<option>上海</option>
<option>广州</option>
<option>深圳</option>
</select><br>
个人描述:
<textarea cols="20" rows="5" name="desc"></textarea><br>
<input type="submit" value="免费注册">
<input type="reset" value="重置">
<input type="button" value="一个按钮">
</form>
</body>
</html>
CSS (Cascading Style Sheet(层叠样式表))
使用 style
标签定义
CSS导入方式
CSS 导入方式其实就是 CSS 代码和 html 代码的结合方式。CSS 导入 HTML有三种方式:
- 内联样式:在标签内部使用style属性,属性值是CSS属性键值对
<div style="color: red">Hello CSS~</div>
给方式只能作用在这一个标签上,如果其他的标签也想使用同样的样式,那就需要在其他标签上写上相同的样式。复用性太差。
- 内部样式:定义
<style>
标签,在标签内部定义CSS样式
<style type="text/css">
span{
color: red;
}
</style>
这种方式可以做到在该页面中复用,被span标签包裹的字体会变为红色
- 外部样式:定义link标签,引入外部的CSS文件
编写一个CSS文件。名为:demo.css,内容如下:
div{
color: red;
}
在html中引入 CSS 文件。
<link rel="stylesheet" href="demo.css">
这种方式可以在多个页面进行复用。其他的页面想使用同样的样式,只需要使用 link
标签引入该CSS文件。
CSS选择器
CSS 选择器就是选取需设置样式的元素(标签),比如如下CSS代码:
div {
color:red;
}
如上代码中的 div
就是 css 中的选择器。这里只讲下面三种选择器:
- 元素选择器
格式:
元素名称{color: red;}
举例:
div {color:red} /*该代码表示将页面中所有的div标签的内容的颜色设置为红色*/
- id选择器
格式:
#id属性值{color: red;}
举例:
#name{color: red;}/*该代码表示将页面中所有的id属性值是 name 的标签的内容的颜色设置为红色*/
在HTML中调用
<div id="name">hello css2</div>
- 类选择器
格式:
.class属性值{color: red;}
举例:
.cls{color: red;} /*该代码表示将页面中所有的class属性值是 cls 的标签的内容的颜色设置为红色*/
在HTML中调用
<div class="cls">hello css3</div>
JavaScript
Web核心
HTTP (HyperText Transfer Protocol)
JavaWeb---HTTP&Tomcat&Servlet | Kyle's Blog (cyborg2077.github.io)
HTTP协议特点
HTTP协议有它自己的一些特点,分别是:
基于TCP协议: 面向连接,安全
TCP是一种面向连接的(建立连接之前是需要经过三次握手)、可靠的、基于字节流的传输层通信协议,在数据传输方面更安全。
基于请求-响应模型的:一次请求对应一次响应
请求和响应是一一对应关系
HTTP协议是无状态协议:对于事物处理没有记忆能力。每次请求-响应都是独立的
无状态指的是客户端发送HTTP请求给服务端之后,服务端根据请求响应数据,响应完后,不会记录任何信息。这种特性有优点也有缺点,
- 缺点:多次请求间不能共享数据
- 优点:速度快
请求之间无法共享数据会引发的问题,如:
- 京东购物,
加入购物车
和去购物车结算
是两次请求, - HTTP协议的无状态特性,加入购物车请求响应结束后,并未记录加入购物车是何商品
- 发起去购物车结算的请求后,因为无法获取哪些商品加入了购物车,会导致此次请求无法正确展示数据
但实际使用的时候,我们发现京东是可以正常展示数据的,原因是Java早已考虑到这个问题,并提出了使用会话技术(Cookie、Session)
来解决这个问题。具体如何来做,后面会详细讲到。刚才提到HTTP协议是规定了请求和响应数据的格式,那具体的格式是什么呢?
请求数据格式
Host: 表示请求的主机名
User-Agent: 浏览器版本,例如Chrome浏览器的标识类似Mozilla/5.0 ...Chrome/79,IE浏览器的标识类似Mozilla/5.0 (Windows NT ...)like Gecko;
Accept:表示浏览器能接收的资源类型,如text/*,image/*或者*/*表示所有;
Accept-Language:表示浏览器偏好的语言,服务器可以据此返回不同语言的网页;
Accept-Encoding:表示浏览器可以支持的压缩类型,例如gzip, deflate等。
- GET和POST两个请求之间的区别:
- GET请求请求参数在请求行中,没有请求体,POST请求请求参数在请求体中
- GET请求请求参数大小有限制,POST没有
Tomcat
什么是Web服务器?
Web服务器是一个应用程序(软件),对HTTP协议的操作进行封装,使得程序员不必直接对协议进行操作,让Web开发更加便捷。主要功能是"提供网上信息浏览服务"。
我们可以把自己写的Web项目部署到Web Tomcat服务器软件中,当Web服务器软件启动后,部署在Web服务器软件中的页面就可以直接通过浏览器来访问了。
Tomcat的相关概念:
Tomcat是Apache软件基金会一个核心项目,是一个开源免费的轻量级Web服务器,支持Servlet/JSP少量JavaEE规范。
概念中提到了JavaEE规范,那什么又是JavaEE规范呢?
JavaEE: Java Enterprise Edition,Java企业版。指Java企业级开发的技术规范总和。包含13项技术规范:JDBC、JNDI、EJB、RMI、JSP、Servlet、XML、JMS、Java IDL、JTS、JTA、JavaMail、JAF。
因为Tomcat支持Servlet/JSP规范,所以Tomcat也被称为Web容器、Servlet容器。Servlet需要依赖Tomcat才能运行。
配置:
控制台乱码:Tomcat默认UTF-8、windows命令行默认GBK
conf/logging.properties
java.util.logging.ConsoleHandler.encoding = 🚩UTF-8
修改端口号:
conf/server.xml
<Connector port=🚩"8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
maxParameterCount="1000"
/>

IDEA中使用Tomcat
创建MavenWeb项目 方法一:使用骨架
- 新建Maven模块,并选择骨架

- 补齐main和resources目录

创建MavenWeb项目 方法二:不使用骨架
- 在pom.xml中添加
<packaging>war</packaging>
- 补齐webapp目录 在项目结构facets中

- web.xml:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
</web-app>
法一:本地Tomcat集成到IDEA




法二:Tomcat的Maven插件
- maven中添加插件
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>7777</port> <!-- 设置tomcat端口,默认值为8080,可以根据需要自行更改 -->
<path>/</path> <!-- 设置tomcat根路径,默认值为/,可以根据需要自行更改 -->
</configuration>
</plugin>
</plugins>
</build>
maven helper插件中run
image-20240319210826735
Servlet
实现的类复写Service方法
- Servlet依赖坐标
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<!--
此处为什么需要添加该标签?
provided指的是在编译和测试过程中有效,最后生成的war包时不会加入
因为Tomcat的lib目录中已经有servlet-api这个jar包,如果在生成war包的时候生效就会和Tomcat中的jar包冲突,导致报错
-->
<scope>provided</scope>
</dependency>
生命周期
生命周期: 对象的生命周期指一个对象从被创建到被销毁的整个过程。
Servlet运行在Servlet容器(web服务器)中,其生命周期由容器来管理,分为4个阶段:
- 加载和实例化:默认情况下,当Servlet第一次被访问时,由容器创建Servlet对象
默认情况,Servlet会在第一次访问被容器创建,但是如果创建Servlet比较耗时的话,那么第一个访问的人等待的时间就比较长,用户的体验就比较差,那么我们能不能把Servlet的创建放到服务器启动的时候来创建,具体如何来配置?
@WebServlet(urlPatterns = "/demo1",loadOnStartup = 1)
loadOnstartup的取值有两类情况
(1)负整数:第一次访问时创建Servlet对象
(2)0或正整数:服务器启动时创建Servlet对象,数字越小优先级越高- 初始化:在Servlet实例化之后,容器将调用Servlet的init()方法初始化这个对象,完成一些如加载配置文件、创建连接等初始化的工作。该方法只调用一次
- 请求处理:每次请求Servlet时,Servlet容器都会调用Servlet的service()方法对请求进行处理
- 服务终止:当需要释放内存或者容器关闭时,容器就会调用Servlet实例的destroy()方法完成资源的释放。在destroy()方法调用之后,容器会释放这个Servlet实例,该实例随后会被Java的垃圾收集器所回收
- 加载和实例化:默认情况下,当Servlet第一次被访问时,由容器创建Servlet对象
方法
init()
service()
destroy()
获取Servlet信息
String getServletInfo()
//该方法用来返回Servlet的相关信息,没有什么太大的用处,一般我们返回一个空字符串即可
public String getServletInfo() {
return "";
}
- 获取ServletConfig对象
ServletConfig getServletConfig()
体系结构
- 编写一个Servlet就必须要实现Servlet接口,重写接口中的5个方法
这样比较麻烦,只有Service方法比较关注

因为我们将来开发B/S架构的web项目,都是针对HTTP协议,所以我们自定义Servlet,会通过继承HttpServlet
@WebServlet("/")
public class HttpServletTest extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("get...");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("post...");
}
HttpServlet类包含了7种方法
HttpServlet的使用步骤
继承HttpServlet
重写doGet和doPost方法
HttpServlet原理
获取请求方式,并根据不同的请求方式,调用不同的doXxx方法
urlPattern 访问路径
- 可以配置多个路径
@WebServlet(urlPatterns = {"/demo1","/demo2"})
配置规则
精确匹配
@WebServlet(urlPatterns = "/user/select")
目录匹配
@WebServlet(urlPatterns = "/user/*")
扩展名匹配
@WebServlet(urlPatterns = "*.do") //注意这里没加斜杠
任意匹配
@WebServlet(urlPatterns = "/") //或 @WebServlet(urlPatterns = "/*")
注意:/
和/*
的区别?
- 当我们的项目中的Servlet配置了 “/”,会覆盖掉tomcat中的DefaultServlet,当其他的url-pattern都匹配不上时都会走这个Servlet
- 当我们的项目中配置了"/*",意味着匹配任意访问路径
- DefaultServlet是用来处理静态资源,如果配置了"/"会把默认的覆盖掉,就会引发请求静态资源的时候没有走默认的而是走了自定义的Servlet类,最终导致静态资源不能被访问(不必太过担心这种问题,因为我们几乎不会配置这种匹配方式)
小结
- urlPattern总共有四种配置方式,分别是精确匹配、目录匹配、扩展名匹配、任意匹配
- 五种配置的优先级为 精确匹配 > 目录匹配> 扩展名匹配 > /* > / ,无需记,以最终运行结果为准。
XML配置
3.0版本前只支持xml配置
package com.itheima.web; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebServlet; public class ServletDemo extends MyHttpServlet { @Override protected void doGet(ServletRequest req, ServletResponse res) { System.out.println("demo get..."); } @Override protected void doPost(ServletRequest req, ServletResponse res) { } }
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!-- Servlet 全类名 --> <servlet> <!-- servlet的名称,名字任意--> <servlet-name>demo13</servlet-name> <!--servlet的类全名--> <servlet-class>com.itheima.web.ServletDemo13</servlet-class> </servlet> <!-- Servlet 访问路径 --> <servlet-mapping> <!-- servlet的名称,要和上面的名称一致--> <servlet-name>demo13</servlet-name> <!-- servlet的访问路径--> <url-pattern>/demo13</url-pattern> </servlet-mapping> </web-app>
Request Response
JavaWeb---Request&Response | Kyle's Blog (cyborg2077.github.io)
- request:获取请求数据
- 浏览器会发送HTTP请求到后台服务器(Tomcat)
- HTTP的请求中会包含很多请求数据(请求行+请求头+请求体)
- 后台服务器(Tomcat)会对HTTP请求的数据进行解析,并把解析解惑存入到一个对象中
- 所存入的对象即为request对象,我们可以从request对象中获取请求的相关参数
- 获取到数据后就可以继续后续的业务,比如获取用户名和密码就可以实现登录操作的相关业务
- response:设置响应数据
- 业务处理完后,后台就需要给前端返回业务处理的结果,即响应数据
- 把响应数据封装到response对象中
- 后台服务器(Tomcat)会解析response对象,按照
响应行+响应头+响应体
的格式拼接结果 - 浏览器最终解析结果,把内容展示在浏览器给用户浏览
Request继承体系

- Request的继承体系为ServletRequest–>HttpServletRequest–>RequestFacade
- Tomcat需要解析请求数据,封装为request对象,并且创建request对象传递到service方法
- 使用request对象,可以查阅JavaEE API文档的HttpServletRequest接口中方法说明
Request获取请求数据
获取请求行数据
请求行包含三块内容,分别是请求方式
、请求资源路径
、HTTP协议及版本
,例如GET /tomcat_demo_war/index.html?username=suger1201&password=dsaasd HTTP/1.1
对于这三部分内容,request对象都提供了对应的API方法来获取,具体如下:
- 获取请求方式:
GET
String getMethod()
- 获取虚拟目录(项目访问路径):
/request-demo
String getContextPath()
- 获取URL(统一资源定位符):
http://localhost:8080/request-demo/req1
StringBuffer getRequestURL()
- 获取URI(统一资源标识符):
/request-demo/req1
String getRequestURI()
- 获取请求参数(GET方式):
username=zhangsan&password=123
String getQueryString()
获取请求头数据
对于请求头的数据,格式为key: value
所以根据请求头名称获取对应值的方法为
String getHeader(String name)
获取请求体数据
浏览器在发送GET请求的时候是没有请求体的,所以需要把请求方式变更为POST
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/web_demo_war_exploded/demo" method="post">
用户名:<input type="text" name="username">
<input type="submit" value="提交">
</form>
</body>
</html>
对于请求体中的数据,Request对象提供了如下两种方式来获取其中的数据,分别是:
- 获取字节输入流,如果前端发送的是字节数据,比如传递的是文件数据,则使用该方法
ServletInputStream getInputStream()
- 获取字符输入流,如果前端发送的是纯文本数据,则使用该方法
BufferedReader getReader()
获取请求参数的通用方式

- 获取所有参数Map集合
Map<String,String[]> getParameterMap()
- 根据名称(Key)获取参数值(Value)(数组)
String[] getParameterValues(String name)
- 根据名称(Key)获取参数值(Value)(单个值)
String getParameter(String name)
post中文乱码:
//1.解决乱码:P0ST.getReader()
request.setCharacterEncoding("UTF-8");//设置字符输入流的编码
get中文乱码:
浏览器不支持中文,所以向服务器发送请求回进行URL编码 %E5%BC%A0%E4%B8%89
Tomcat默认URL解码用的是ISO-8859-1,产生了乱码
- URL编码
1.将字符串按照编码方式转为二进制
2.每个字节转为2个16进制数并在前边加上%
//1.URL编码
String encode URLEncoder.encode(username,enc:"utf-8");
System.out.println(encode);
//2.URL解码
String decode URLDecoder.decode(encode,enc:"utf-8");
System.out.println(decode);
🚩🚩🚩//3.转换为字节数据
byte[]bytes decode.getBytes(charsetName:"ISO-8859-1");
/*for (byte b bytes){
System.out.print(b ")
}*/
//4.将字节数组转为字符串
String s new String(bytes,charsetName:"utf-8");
System.out.println(s);
解决方法:把乱码的字符串转换成字节,把字节再重新编码成字符串
Tomcat8.0之后,已将GET请求乱码问题解决,设置默认的解码方式为UTF-8
Request转发请求
请求转发(forward):一种在服务器内部的资源跳转方式。
- 浏览器发送请求给服务器,服务器中对应的资源A接收到请求
- 资源A处理完请求后将请求发给资源B
- 资源B处理完后将结果响应给浏览器
- 请求从资源A到资源B的过程就叫请求转发
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.IOException;
@WebServlet("/RequestDemo1")
public class RequestDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("这里是RequestDemo1");
//加上这行
🚩request.getRequestDispatcher("/RequestDemo2").forward(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
在转发的同时我们还可以传递数据给/RequestDemo2
request对象提供了三个方法:
- 存储数据到request域[范围,数据是存储在request对象]中
void setAttribute(String name,Object o);
- 根据key获取值
Object getAttribute(String name);
- 根据key删除该键值对
void removeAttribute(String name);
@WebServlet("/RequestDemo1")
public class RequestDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("这里是RequestDemo1");
//存储数据
🚩🚩🚩request.setAttribute("msg","HELLO~");
//请求转发
request.getRequestDispatcher("/RequestDemo2").forward(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
@WebServlet("/RequestDemo2")
public class RequestDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("这里是RequestDemo2");
🚩🚩🚩Object msg = request.getAttribute("msg");
System.out.println(msg);
}
请求转发的特点
- 浏览器地址栏路径不发生变化
虽然后台从/这里是RequestDemo1
转发到/这里是RequestDemo2
,但是浏览器的地址一直是/这里是RequestDemo1
,未发生变化 - 只能转发到当前服务器的内部资源
不能从一个服务器通过转发访问另一台服务器 - 一次请求,可以在转发资源间使用request共享数据
虽然后台从/RequestDemo1
转发到/RequestDemo2
,但是这个只有一次请求
Response 继承体系

Response重定向
Response设置响应数据功能介绍
HTTP响应数据总共分为三部分内容,分别是响应行、响应头、响应体,对于这三部分内容的数据,respone对象都提供了哪些方法来进行设置?
- 响应行
响应行包含三块内容,分别是 HTTP/1.1[HTTP协议及版本] 200[响应状态码] ok[状态码的描述]
对于响应头,比较常用的就是设置响应状态码:
void setStatus(int sc);
- 响应头
响应头的格式为key:value形式
设置响应头键值对:
void setHeader(String name,String value);
- 响应体
对于响应体,是通过字符、字节输出流的方式往浏览器写,
获取字符输出流:
PrintWriter getWriter();
获取字节输出流
ServletOutputStream getOutputStream();
Response请求重定向
- Response重定向(redirect):一种资源跳转方式。
- 浏览器发送请求给服务器,服务器中对应的资源A接收到请求
- 资源A现在无法处理该请求,就会给浏览器响应一个302的状态码+location的一个访问资源B的路径(要加上资源B的虚拟目录)
- 浏览器接收到响应状态码为302就会重新发送请求到location对应的访问地址去访问资源B
- 资源B接收到请求后进行处理并最终给浏览器响应结果,这整个过程就叫重定向
- 重定向的实现方式
response.setStatus(302);
response.setHeader("location","资源B的访问路径");
//或
resposne.sendRedirect("资源B的访问路径");

package response;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/rep1")
public class ResponseDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("这里是ResponseDemo1");
response.sendRedirect("rep2");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
package response;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/rep2")
public class ResponseDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("这里是ResponseDemo2");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}

重定向的特点
- 浏览器地址栏路径发送变化
当进行重定向访问的时候,由于是由浏览器发送的两次请求,所以地址会发生变化 - 可以重定向到任何位置的资源(服务内容、外部均可)
因为第一次响应结果中包含了浏览器下次要跳转的路径,所以这个路径是可以任意位置资源。 - 两次请求,不能在多个资源使用request共享数据
因为浏览器发送了两次请求,是两个不同的request对象,就无法通过request对象进行共享数据
介绍完请求重定向
和请求转发
以后,接下来把这两个放在一块对比下:
重定向特点 | 请求转发特点 |
---|---|
浏览器地址栏路径发生变化 | 浏览器地址栏路径不发生变化 |
可以重定向到任意位置的资源(服务器内部、外部均可) | 只能转发到当前服务器的内部资源 |
两次请求,不能在多个资源使用request共享数据 | 一次请求,可以在转发的资源间使用request共享数据 |
资源路径问题
问题1:转发的时候路径上没有加虚拟目录
web_demo_war_exploded
,而重定向加了,那么到底什么时候需要加,什么时候不需要加呢?其实判断的依据很简单,只需要记住下面的规则即可:
- 浏览器使用:需要加虚拟目录(项目访问路径)
- 服务端使用:不需要加虚拟目录
对于转发来说,因为是在服务端进行的,所以不需要加虚拟目录
对于重定向来说,路径最终是由浏览器来发送请求,就需要添加虚拟目录。
掌握了这个规则,接下来就通过一些练习来强化下知识的学习:对于转发来说,因为是在服务端进行的,所以不需要加虚拟目录
对于重定向来说,路径最终是由浏览器来发送请求,就需要添加虚拟目录。
Q:
<a href='路径'>
<form action='路径'>
req.getRequestDispatcher("路径")
resp.sendRedirect("路径")
A:
1.超链接,从浏览器发送,需要加
2.表单,从浏览器发送,需要加
3.转发,是从服务器内部跳转,不需要加
4.重定向,是由浏览器进行跳转,需要加。
- 我们可以去pom.xml配置文件中配置项目的访问地址,然后在代码中动态去获取项目访问的虚拟目录,request对象中提供了getContextPath()方法
@WebServlet("/rep1")
public class ResponseDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("这里是ResponseDemo1");
//获取虚拟目录
🚩🚩🚩String contextPath = request.getContextPath();
//把虚拟目录拼在前面
response.sendRedirect(contextPath + "/rep2");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
Response响应字符数据
要想将字符数据写回到浏览器,我们需要两个步骤:
- 通过Response对象获取字符输出流: PrintWriter writer = resp.getWriter();
- 通过字符输出流写数据: writer.write(“aaa”);
@WebServlet("/RequestDemo1")
public class RequestDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
PrintWriter printWriter = response.getWriter();
printWriter.write("hello world");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
@WebServlet("/RequestDemo1")
public class RequestDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//设置响应的数据格式及数据的编码
🚩🚩🚩response.setContentType("text/html;charset=utf-8");
PrintWriter printWriter = response.getWriter();
printWriter.write("<h1>你好<h1>");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
Response响应字节数据
要想将字节数据写回到浏览器,我们需要两个步骤:
- 通过Response对象获取字节输出流:ServletOutputStream outputStream = resp.getOutputStream();
- 通过字节输出流写数据: outputStream.write(字节数据);
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.FileInputStream;
import java.io.IOException;
@WebServlet("/RequestDemo1")
public class RequestDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
FileInputStream fis = new FileInputStream("D:\\background.jpg");
ServletOutputStream os = response.getOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = fis.read(buffer)) != -1){
os.write(buffer,0,len);
}
//也可以用现成的🚩🚩🚩IOUtils.copy(fis,os);
fis.close();
//不需要关闭ServletOutputStream,response会帮我们关闭
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
IOUtils工具类使用
1.导入坐标
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
2.使用
lOUtils.copy(输入流,输出流);
用户注册案例
用户登录

- LoginServlet
package login;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import user.User;
import user.UserMapper;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取请求体
String userName = req.getParameter("username");
String password = req.getParameter("password");
System.out.println(userName);
System.out.println(password);
//1. 加载MyBatis核心配置文件,获取SqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//3. 获取Mapper接口的代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//4. 执行方法
User user = userMapper.select(userName, password);
resp.setContentType("text/html;charset=utf-8");
PrintWriter printWriter = resp.getWriter();
if (user != null ) {printWriter.write("<h1>登录成功<h1>"); }
else {printWriter.write("<h1>登录失败<h1>");}
//5. 资源关闭
sqlSession.close();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
- User
package user;
import java.util.Objects;
public class User {
private String userName;
private String password;
public User(String userName, String password) {
this.userName = userName;
this.password = password;
}
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", password='" + password + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
User user = (User) o;
return Objects.equals(userName, user.userName) && Objects.equals(password, user.password);
}
@Override
public int hashCode() {
return Objects.hash(userName, password);
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
- UserMapper
package user;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface UserMapper {
@Select(value = "select * from tb_user where id = #{id}")
public User selectByID(int id);
@Select(value = "select * from tb_user where username = #{userName} and password = #{password}")
public User select(@Param("userName") String userName, @Param("password") String password);
}
- mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<!--数据库的连接信息-->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///learn?useSSL=false"/>
<property name="username" value="cdd"/>
<property name="password" value="1"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--加载SQL映射文件-->
<package name="user"/>
</mappers>
</configuration>
- web.xml
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
</web-app>
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
</body>
</html>
- pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>LearnWeb</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>L_Response</artifactId>
<packaging>war</packaging>
<name>L_Response Maven Webapp</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<!--
此处为什么需要添加该标签?
provided指的是在编译和测试过程中有效,最后生成的war包时不会加入
因为Tomcat的lib目录中已经有servlet-api这个jar包,如果在生成war包的时候生效就会和Tomcat中的jar包冲突,导致报错
-->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.6</version>
</dependency>
<!-- 添加slf4j日志api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.20</version>
</dependency>
<!-- 添加logback-classic依赖 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<!-- 添加logback-core依赖 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>7777</port> <!-- 设置tomcat端口,默认值为8080,可以根据需要自行更改 -->
<path>/</path> <!-- 设置tomcat根路径,默认值为/,可以根据需要自行更改 -->
</configuration>
</plugin>
</plugins>
<finalName>L_Response</finalName>
</build>
</project>
用户注册

- RegisterServlet
package login;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import user.User;
import user.UserMapper;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
@WebServlet("/register")
public class RegisterServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取请求体
🚩🚩🚩String userName = req.getParameter("username");
🚩🚩🚩String password = req.getParameter("password");
System.out.println(userName);
System.out.println(password);
//1. 加载MyBatis核心配置文件,获取SqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//3. 获取Mapper接口的代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//4. 执行方法
User user = userMapper.selectByName(userName);
🚩🚩🚩resp.setContentType("text/html;charset=utf-8");
PrintWriter printWriter = resp.getWriter();
if (user != null) {
printWriter.write("<h1>用户名已存在<h1>");
} else {
int res = userMapper.insert(userName, password);
System.out.println(res);
if (res>0) {
printWriter.write("<h1>注册成功<h1>");
}
else {printWriter.write("<h1>注册失败<h1>");}
}
🚩🚩🚩sqlSession.commit();
//5. 资源关闭
sqlSession.close();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
- UserMapper
package user;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface UserMapper {
@Select(value = "select * from tb_user where username = #{userName}")
public User selectByName(String userName);
@Select(value = "select * from tb_user where username = #{userName} and password = #{password}")
public User select(@Param("userName") String userName, @Param("password") String password);
// 不想写全部字段的话,id可以填null
@Insert(value = "insert into tb_user(username,password) values (#{userName}, #{password})")
public 🚩🚩🚩int insert(@Param("userName") String userName, @Param("password") String password);
// 写成这样也可以
@Insert(value = "insert into tb_user(username,password) values (#{userName}, #{password})")
public int insert(User u);
}
使用
@Param
注解的方法(方法1): 这种方法在@Insert
注解的 SQL 语句中直接使用了命名的占位符#{userName}
和#{password}
,并且在 Mapper 接口的方法中通过@Param
注解为这些占位符指定了参数名称。这样,MyBatis 就能够将方法的参数与 SQL 语句中的占位符正确地关联起来。@Insert(value = "insert into tb_user(username, password) values (#{userName}, #{password})") public int insert(@Param("userName") String userName, @Param("password") String password);
这种方法的好处是参数名称在接口方法和 SQL 语句中保持一致,这使得代码更容易理解和维护。
使用对象作为参数的方法(方法2): 在这种方法中,您创建了一个
User
类的实例作为参数,并在@Insert
注解的 SQL 语句中使用了对象属性作为占位符。MyBatis 会根据User
类的属性名称来自动映射 SQL 语句中的占位符。public int insert(User u);
当您调用这个方法时,MyBatis 会查找
User
类中名为userName
和password
的属性,并将它们与 SQL 语句中的#{userName}
和#{password}
占位符进行匹配。这种方式简化了代码,因为您不需要在接口方法中显式声明每个参数,但前提是您的User
类必须包含与数据库表结构相匹配的属性,并且属性名称要与 SQL 语句中的占位符名称一致。
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>欢迎注册</h1>
<a href="login.html">已有账号?点击登录</a>
<form action="/register" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username" id="username"></td>
<br>
<span id="username_arr" style="display: none">用户名已被占用</span>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password" id="password"></td>
</tr>
</table>
<input type="submit" value="注册">
</form>
</body>
</html>
- login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
</body>
</html>
SqlSessionFactory工具类抽取
写Servlet的时候,因为需要使用Mybatis来完成数据库的操作,所以对于Mybatis的基础操作就出现了些重复代码:
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
造成的问题:
- 重复代码不利于后期的维护
- SqlSessionFactory工厂类进行重复创建
- 就相当于每次买手机都需要重新创建一个手机生产工厂来给你制造一个手机一样,资源消耗非常大但性能却非常低。所以这么做是不允许的。
优化:
- 代码重复可以抽取工具类
- 对指定代码只需要执行一次可以使用静态代码块
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class SqlSessionFactoryUtils {
private static SqlSessionFactory sqlSessionFactory;
static {
try {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static SqlSessionFactory getSqlSessionFactory(){
return sqlSessionFactory;
}
}
之后调用:
SqlSessionFactory sqlSessionFactory =SqlSessionFactoryUtils.getSqlSessionFactory();
JSP
一种动态的网页技术,其中既可以定义HTML、JS、CSS等静态内容,还可以定义Java代码的动态内容
- 导入jsp标签
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<scope>provided</scope>
</dependency>
- 编写jsp文件
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>hello jsp</h1>
<%
System.out.println("HELLO JSP");
%>
</body>
</html>
本质上是Servlet
JSP脚本分类
JSP 脚本有如下三个分类:
- <%…%>:内容会直接放到_jspService()方法之中
- <%=…%>:内容会放到out.print()中,作为out.print()的参数,此方法可以将Java代码中获取的数据动态的展示到页面中
- <%!…%>:内容会放到_jspService()方法之外,被类直接包含
JSP缺点
由于 JSP页面内,既可以定义 HTML 标签,又可以定义 Java代码,造成了以下问题:
- 书写麻烦:特别是复杂的页面,既要写 HTML 标签,还要写 Java 代码
- 阅读麻烦
- 复杂度高:运行需要依赖于各种环境,JRE,JSP容器,JavaEE…
- 占内存和磁盘:JSP会自动生成.java和.class文件占磁盘,运行的是.class文件占内存
- 调试困难:出错后,需要找到自动生成的.java文件进行调试
- 不利于团队协作:前端人员不会 Java,后端人员不精 HTML
- 如果页面布局发生变化,前端工程师对静态页面进行修改,然后再交给后端工程师,由后端工程师再将该页面改为 JSP 页面
基于上述的问题, JSP 已逐渐退出历史舞台,以后开发更多的是使用 HTML
+ Ajax
来替代。Ajax
是后续重点学习的技术。有个这个技术后,前端工程师负责前端页面开发,而后端工程师只负责前端代码开发。下来对技术的发展进行简单的说明
EL表达式
概述
EL(全称Expression Language )表达式语言,用于简化 JSP 页面内的 Java 代码。
EL 表达式的主要作用是 获取数据。其实就是从域对象中获取数据,然后将数据展示在页面上。
而 EL 表达式的语法也比较简单,${expression}
。例如:${brands}
就是获取域中存储的 key 为 brands 的数据。
@WebServlet("demo1")
public class ServletDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//准备数据
List<Brand> brands = new ArrayList<Brand>();
brands.add(new Brand(1, "三只松鼠", "三只松鼠", 100, "三只松鼠,好吃不上火", 1));
brands.add(new Brand(2, "优衣库", "优衣库", 200, "优衣库,服适人生", 0));
brands.add(new Brand(3, "小米", "小米科技有限公司", 1000, "为发烧而生", 1));
//存储到request域中
request.setAttribute("brands", brands);
//转发到el-demo.jsp
request.getRequestDispatcher("/el-demo.jsp").forward(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
注意:此处需要用转发,因为转发才可以使用 request 对象作为域对象进行数据共享
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page isELIgnored="false" %>
<html>
<head>
<title>Title</title>
</head>
<body>
${brands}
</body>
</html>
可能遇到的问题:页面上只输出了${brands},而不是具体的数据
解决方案:在JSP页面头加上<%@ page isELIgnored="false" %>
域对象
JavaWeb中有四大域对象,分别是:
- page:当前页面有效
- request:当前请求有效
- session:当前会话有效
- application:当前应用有效
el 表达式获取数据,会依次从这4个域中寻找(域范围依次增大,层层包裹),直到找到为止。
例如: ${brands},el 表达式获取数据,会先从page域对象中获取数据,如果没有再到 requet 域对象中获取数据,如果再没有再到 session 域对象中获取,如果还没有才会到 application 中获取数据。
JSTL标签
概述
JSP标准标签库(Jsp Standarded Tag Library) ,使用标签取代JSP页面上的Java代码。如下代码就是JSTL标签
<c:if test="${flag == 1}">
男
</c:if>
<c:if test="${flag == 2}">
女
</c:if>
导入坐标
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
在JSP页面上引入JSTL标签库
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
if标签
<c:if>
:相当于 if 判断
- 属性:test,用于定义条件表达式
<c:if test="${flag == 1}">
男
</c:if>
<c:if test="${flag == 2}">
女
</c:if>
forEach标签
<c:forEach>
:相当于 for 循环。java中有增强for循环和普通for循环,JSTL 中的 <c:forEach>
也有两种用法
用法一
类似于 Java 中的增强for循环。涉及到的 <c:forEach>
中的属性如下
- items:被遍历的容器
- var:遍历产生的临时变量
- varStatus:遍历状态对象
如下代码,是从域对象中获取名为 brands 数据,该数据是一个集合;遍历遍历,并给该集合中的每一个元素起名为 brand
,是 Brand对象。在循环里面使用 EL表达式获取每一个Brand对象的属性值
<c:forEach items="${brands}" var="brand">
<tr align="center">
<td>${brand.id}</td>
<td>${brand.brandName}</td>
<td>${brand.companyName}</td>
<td>${brand.description}</td>
</tr>
</c:forEach>
这里的${brand.id}
,并不是直接获取的属性的id,而是调用的brand的getId()方法,底层实现是先将首字母大写,由id
变成Id
,然后在前面拼接一个get,从而变成getId
,随后调用brand的getId()
用法二
类似于 Java 中的普通for循环。涉及到的 <c:forEach>
中的属性如下
- begin:开始数
- end:结束数
- step:步长
实例代码:
从0循环到10,变量名是 i
,每次自增1
<c:forEach begin="0" end="10" step="1" var="i">
${i}
</c:forEach>
MVC格式与三层架构
三层架构

- 表现层:接收请求,封装数据,调用业务逻辑层,响应数据
- 业务逻辑层:对业务逻辑进行封装,组合数据访问层层中基本功能,形成复杂的业务逻辑功能。例如
注册业务功能
,我们会先调用数据访问层
的selectByName()
方法判断该用户名是否存在,如果不存在再调用数据访问层
的insert()
方法进行数据的添加操作 - 数据访问层:对数据库的CRUD基本操作
MVC格式和三层架构

案例
Kyle's Blog (cyborg2077.github.io)
会话跟踪
会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次
请求和响应
会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求之间共享数据
- 服务器会收到多个请求,这多个请求可能来自多个浏览器(京东这种购物网站,肯定时时刻刻都有好多人访问)
- 服务器需要用来识别请求是否来自同一个浏览器
- 服务器用来识别浏览器的过程,这个过程就是
会话跟踪
- 服务器识别浏览器后就可以在同一个会话中的多次请求之间来
共享数据
那为什么现在浏览器和服务器不支持数据共享呢?
- 浏览器和服务器之间使用的是HTTP请求来进行数据传输
- HTTP协议是无状态的,每次浏览器向服务器请求时,服务器都会将该请求视为新的请求
- HTTP协议设计成无状态的目的是让每次请求之间相互独立,互不影响
- 请求与请求之间独立后,就无法实现多次请求之间的数据共享
具体的实现方式有:
- 客户端会话跟踪技术:Cookie
- 服务端会话跟踪技术:Session
Cookie
Cookie的基本使用
对于Cookie的使用,更关注的应该是后台代码如何操作Cookie,对于Cookie的操作主要分两大类,分别是发送Cookie
和获取Cookie
- 发送Cookie
Cookie cookie = new Cookie("key","value");
response.addCookie(cookie);
- maven依赖
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
</dependency>
- 存入数据
@WebServlet("/aServlet")
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Cookie cookie = new Cookie("username","zs");
response.addCookie(cookie);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
- 获取cookie
@WebServlet("/bServlet")
public class BServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
String name = cookie.getName();
if ("username".equals(name)) {
String value = cookie.getValue();
System.out.println(name + ":" + value);
}
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
小结
- 发送Cookie:
- 创建Cookie对象,并设置值:
Cookie cookie = new Cookie("key","value");
- 发送Cookie到客户端使用的是Reponse对象:
response.addCookie(cookie);
- 创建Cookie对象,并设置值:
- 获取Cookie:
- 使用Request对象获取Cookie数组:
Cookie[] cookies = request.getCookies();
- 遍历数组
- 获取数组中每个Cookie对象的值:
cookie.getName()
和cookie.getValue()
- 使用Request对象获取Cookie数组:
原理
Cookie的实现原理是基于HTTP协议的,其中涉及到HTTP协议中的两个请求头信息:
- 响应头:set-cookie
- 请求头:cookie
- 前面的案例中已经能够实现,AServlet给前端发送Cookie,BServlet从request中获取Cookie的功能
- 对于AServlet响应数据的时候,Tomcat服务器都是基于HTTP协议来响应数据
- 当Tomcat发现后端要返回的是一个Cookie对象之后,Tomcat就会在响应头中添加一行数据
Set-Cookie:username=zs
- 浏览器获取到响应结果后,从响应头中就可以获取到
Set-Cookie
对应值username=zs
,并将数据存储在浏览器的内存中 - 浏览器再次发送请求给BServlet的时候,浏览器会自动在请求头中添加
Cookie: username=zs
发送给服务端BServlet - Request对象会把请求头中cookie对应的值封装成一个个Cookie对象,最终形成一个数组
- BServlet通过Request对象获取到Cookie[]后,就可以从中获取自己需要的数据
细节:
- 设置Cookie存活时间
setMaxAge(int seconds);
参数值为:
- 正数:将Cookie写入浏览器所在电脑的硬盘,持久化存储。到时间自动删除
- 负数:默认值,Cookie在当前浏览器内存中,当浏览器关闭,则Cookie被销毁
- 零:删除对应Cookie
@WebServlet("/aServlet")
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Cookie cookie = new Cookie("username", "zs");
cookie.setMaxAge(7 * 24 * 60 * 60); //设置七天的
response.addCookie(cookie);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
- 访问AServlet后,把浏览器关闭重启后访问BServlet,能在控制台打印出
username:zs
,说明Cookie没有随着浏览器关闭而被销毁 - 通过浏览器查看Cookie的内容,到期时间成功续期7天
名称 username
内容 zs
域名 localhost
路径 /cookie_demo
为何发送 仅限同一网站的连接
脚本可访问 是
创建时间 2022年8月20日星期六 10:47:16
到期时间 2022年8月27日星期六 10:47:16
Cookie存中文
在AServlet中对中文进行URL编码,采用URLEncoder.encode(),将编码后的值存入Cookie中
@WebServlet("/aServlet")
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String value = "张三";
//URL编码
value = URLEncoder.encode(value, "UTF-8");
Cookie cookie = new Cookie("username", value);
cookie.setMaxAge(7 * 24 * 60 * 60);
response.addCookie(cookie);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
在BServlet中获取Cookie中的值,获取的值为URL编码后的值(%E5%BC%A0%E4%B8%89
)
名称 username
内容 %E5%BC%A0%E4%B8%89
域名 localhost
路径 /cookie_demo
为何发送 仅限同一网站的连接
脚本可访问 是
创建时间 2022年8月20日星期六 12:25:37
到期时间 2022年8月27日星期六 12:25:37
- 将获取的值在进行URL解码,采用URLDecoder.decode(),就可以获取到对应的中文值,控制台输出
username:张三
@WebServlet("/bServlet")
public class BServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
String name = cookie.getName();
if ("username".equals(name)) {
String value = cookie.getValue();
//解码
value = URLDecoder.decode(value, "utf-8");
System.out.println(name + ":" + value);
}
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
Session
概念
Session:服务端会话跟踪技术:将数据保存到服务端。
- Session是存储在服务端而Cookie是存储在客户端
- 存储在客户端的数据容易被窃取和截获,存在很多不安全的因素
- 存储在服务端的数据相比于客户端来说就更安全
工作流程
- 在服务端的AServlet获取一个Session对象,把数据存入其中
- 在服务端的BServlet获取到相同的Session对象,从中取出数据
- 就可以实现一次会话中多次请求之间的数据共享了
- 现在最大的问题是如何保证AServlet和BServlet使用的是同一个Session对象(后面的Session原理会说)
使用
JavaEE中提供了HttpSession接口,来实现一次会话的多次请求之间数据共享功能
- 获取Session对象
HttpSession session = request.getSession();
Session对象提供的功能:
存储数据到 session 域中
void setAttribute(String name, Object o)
根据 key,获取值
Object getAttribute(String name)
根据 key,删除该键值对
void removeAttribute(String name)
- 存数据
@WebServlet("/demo1")
public class SessionDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取Session对象
HttpSession session = request.getSession();
//存储数据
session.setAttribute("username","ls");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
- 取数据
@WebServlet("/demo2")
public class SessionDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取Session对象
HttpSession session = request.getSession();
//获取数据
Object username = session.getAttribute("username");
//看看能否输出到控制台
System.out.println(username);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
注意:Session中可以存储的是一个Object类型的数据,也就是说Session中可以存储任意数据类型。
原理
Session是基于Cookie实现的
这句话其实不太能详细的说明Session的底层实现,接下来,我们一步步来分析下Session的具体实现原理:
前提条件
- Session要想实现一次会话多次请求之间的数据共享,就必须要保证多次请求获取Session的对象是同一个。
如何验证
- 在上面案例中的两个ServletDemo中分别打印下Session对象,比较其地址值是否相同
ServletDemo1
@WebServlet("/demo1")
public class SessionDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession();
System.out.println(session);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
- ServletDemo2
@WebServlet("/demo2")
public class SessionDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession();
System.out.println(session);
Object username = session.getAttribute("username");
System.out.println(username);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
- 启动测试,分别访问
/demo1
和/demo2
,输出如下
org.apache.catalina.session.StandardSessionFacade@69186990
org.apache.catalina.session.StandardSessionFacade@69186990
通过打印可以得到如下结论:
- 两个Servlet类中获取的Session对象是同一个
- 把demo1和demo2请求刷新多次,控制台最终打印的结果都是同一个
那么问题又来了,如果新开一个浏览器,访问demo1或者demo2,打印在控制台的Session还是同一个对象么?
- 测试的结果:打印的Session不一样。
org.apache.catalina.session.StandardSessionFacade@1c7eb9a
org.apache.catalina.session.StandardSessionFacade@1c7eb9a
所以Session实现的也是一次会话中的多次请求之间的数据共享。
- Session是如何保证在一次会话中获取的Session对象是同一个呢?
- demo1在第一次获取session对象的时候,session对象会有一个唯一的标识,假如是
id:10
- demo1在session中存入其他数据并处理完成所有业务后,需要通过Tomcat服务器响应结果给浏览器
- Tomcat服务器发现业务处理中使用了session对象,就会把session的唯一标识
id:10
当做一个cookie,添加Set-Cookie:JESSIONID=10
到响应头中,并响应给浏览器 - 浏览器接收到响应结果后,会把响应头中的coookie数据存储到浏览器的内存中
- 浏览器在同一会话中访问demo2的时候,会把cookie中的数据按照
cookie: JESSIONID=10
的格式添加到请求头中并发送给服务器Tomcat - demo2获取到请求后,从请求头中就读取cookie中的JSESSIONID值为10,然后就会到服务器内存中寻找
id:10
的session对象,如果找到了,就直接返回该对象,如果没有则新创建一个session对象 - 关闭打开浏览器后,因为浏览器的cookie已被销毁,所以就没有JESSIONID的数据,服务端获取到的session就是一个全新的session对象
使用细节
Session的钝化与活化
首先需要大家思考的问题是:
- 服务器重启后,Session中的数据是否还在?
要想回答这个问题,我们首先应该想到
- 服务器端AServlet和BServlet共用的session对象应该是存储在服务器的内存中
- 服务器重新启动后,内存中的数据应该是已经被释放,对象也应该都销毁了
所以session数据应该也已经不存在了。但是如果session不存在会引发什么问题呢?
举个例子说明下,
- 用户把需要购买的商品添加到购物车,因为要实现同一个会话多次请求数据共享,所以假设把数据存入Session对象中
- 用户正要付钱的时候接到一个电话,付钱的动作就搁浅了
- 正在用户打电话的时候,购物网站因为某些原因需要重启
- 重启后session数据被销毁,购物车中的商品信息也就会随之而消失
- 用户想再次发起支付,就会出为问题
所以说对于session的数据,我们应该做到就算服务器重启了,也应该能把数据保存下来才对。
那么Tomcat服务器在重启的时候,session数据到底会不会保存以及是如何保存的,我们可以通过实际案例来演示下:
- 先启动Tomcat服务器
- 访问
/demo1
将数据存入session中 - Ctrl+C正确停止Tomcat服务器
- 再次重新启动Tomcat服务器
- 访问
/demo2
查看是否能获取到session中的数据
经过测试,会发现只要服务器是正常关闭和启动,session中的数据是可以被保存下来的。
具体的原因就是:Session的钝化和活化:
钝化:在服务器正常关闭后,Tomcat会自动将Session数据写入硬盘的文件中
- 钝化的数据路径为:
项目目录\target\tomcat\work\Tomcat\localhost\项目名称\SESSIONS.ser
- 钝化的数据路径为:
活化:再次启动服务器后,从文件中加载数据到Session中
- 数据加载到Session中后,路径中的
SESSIONS.ser
文件会被删除掉
- 数据加载到Session中后,路径中的
session数据存储在服务端,服务器重启后,session数据会被保存
浏览器被关闭启动后,重新建立的连接就已经是一个全新的会话,获取的session数据也是一个新的对象
session的数据要想共享,浏览器不能关闭,所以session数据不能长期保存数据
cookie是存储在客户端,是可以长期保存
Session的销毁
Session的销毁会有两种方式:
默认情况下,无操作,30分钟自动销毁
- 对于这个失效时间,是可以通过配置进行修改的
- 在项目的web.xml中配置
- 如果没有配置,默认是30分钟,默认值是在Tomcat的web.xml配置文件中写死的
- 对于这个失效时间,是可以通过配置进行修改的
调用Session对象的invalidate()进行销毁
- 在SessionDemo2类中添加session销毁的方法
JAVA @WebServlet("/demo2") public class SessionDemo2 extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session = request.getSession(); //销毁 session.invalidate(); Object username = session.getAttribute("username"); System.out.println(username); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doGet(request, response); } }
- 启动访问测试,先访问demo1将数据存入到session,再次访问demo2从session中获取数据,会得到如下错误信息
java.lang.IllegalStateException: getAttribute: Session already invalidated
- 该销毁方法一般会在用户退出的时候,需要将session销毁掉。
Cookie和Session小结
Cookie 和 Session 都是来完成一次会话内多次请求间数据共享的。所需两个对象放在一块,就需要思考:
Cookie和Session的区别是什么?
- 区别:
- 存储位置:Cookie 是将数据存储在客户端,Session 将数据存储在服务端
- 安全性:Cookie不安全,Session安全
- 数据大小:Cookie最大3KB,Session无大小限制
- 存储时间:Cookie可以通过setMaxAge()长期存储,Session默认30分钟
- 服务器性能:Cookie不占服务器资源,Session占用服务器资源
Cookie和Session的应用场景分别是什么?
- 应用场景:
- 购物车:使用Cookie来存储
- 已登录用户的名称展示:使用Session来存储
- 记住我功能:使用Cookie来存储
- 验证码:使用session来存储
结论
- Cookie是用来保证用户在未登录情况下的身份识别
- Session是用来保存用户登录后的数据
案例
Filter
Filter概述
Filter 表示过滤器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一。我们之前都已经学习过了Servlet ,现在我们来学习Filter和Listener。
过滤器可以把对资源的请求拦截
下来,从而实现一些特殊的功能。
例如某些网站未登录不能查看评论,不能将商品加入购物车,得把你拦下来,先让你登录,也就是在访问前,先经过Filter。
下面来具体说说,拦截器拦截到后可以做什么功能呢?
过滤器一般完成一些通用的操作。比如每个资源都要写一些代码完成某个功能,我们总不能在每个资源中写这样的代码吧,而此时我们可以将这些代码写在过滤器中,因为请求每一个资源都要经过过滤器。
我们之前做的品牌数据管理的案例中就已经做了登陆的功能,但这个登录功能其实是如同虚设的,我们可以直接访问登录后的页面,所以本文的目标就是完善登录功能,不登录就无法查看数据。
Filter入门
开发步骤
进行Filter
开发分为以下三步实现
- 定义类,实现Filter接口,并重写其所有方法
//实现Filter接口,重写所有方法
public class FilterDemo1 implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
}
public void destroy() {
}
}
- 配置Filter拦截路径资源:在类上定义
@WebFilter
注解。而注解的value属性值/*
表示拦截所有资源
//这里暂时先拦截所有资源,后面我们会仔细讲拦截路径的配置
@WebFilter("/*")
public class FilterDemo1 implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
}
public void destroy() {
}
}
- 在doFilter方法中输出一句话,并放行
WebFilter("/*")
public class FilterDemo1 implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//这句话只是来测试是否调用了该方法
System.out.println("doFilter...");
//一定要放行才能访问资源
filterChain.doFilter(servletRequest, servletResponse);
}
public void destroy() {
}
}
代码演示
- 创建一个web项目,在
webapp
下创建hello.jsp
页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<!--随便输出点什么东西-->
<h1>HELLO FILTER</h1>
</body>
</html>
- pom.xml配置如下
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>filter-demo</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>filter-demo Maven Webapp</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mortbay.jetty</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5-20081211</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<finalName>filter-demo</finalName>
</build>
</project>
- 在java目录下新建
web.filter
包,并新建FilterDemo1
类
@WebFilter("/*")
public class FilterDemo1 implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("doFilter...");
filterChain.doFilter(servletRequest, servletResponse);
}
public void destroy() {
}
}
- 测试
- 将放行的代码注释掉,我们访问
hello.jsp
页面,不会有任何内容,因为被拦截了,且没有放行,控制台会输出doFilter...
- 打开放行的代码,我们访问
hello.jsp
页面,会有h1标签正常输出HELLO FILTER
- 将放行的代码注释掉,我们访问
上述效果说明了FilterDemo1这个过滤器的doFilter
方法被执行了,且必须添加放行的方法才能访问hello.jsp
页面
Filter执行流程

如上图是使用过滤器的流程,我们通过以下问题来研究过滤器的执行流程:
- 放行后访问对应资源,资源访问完成后,还会回到Filter中吗?
- 从上图就可以看出肯定会回到Filter中
- 如果回到Filter中,是重头执行还是执行放行后的逻辑呢?
- 如果是重头执行的话,就意味着
放行前逻辑
会被执行两次,肯定不会这样设计了;所以访问完资源后,会回到放行后逻辑
,执行该部分代码。
- 如果是重头执行的话,就意味着
通过上述的说明,我们可以总结一下Filter的执行流程
- 执行放行前逻辑 --> 放行 --> 访问资源 --> 执行放行后逻辑
接下来我们通过代码验证一下,在 doFilter()
方法前后都加上输出语句
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("1.Filter...");
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("3.Filter...");
}
在hello.jsp
中添加输出语句如下
<body>
<h1>HELLO FILTER</h1>
<%
System.out.println("2.Filter...");
%>
</body>
重启服务器,访问hello.jsp
页面,控制台输出结果如下,符合我们的预期结果
1.Filter…
2.Filter…
3.Filter…
Filter拦截路径配置
拦截路径表示 Filter 会对请求的哪些资源进行拦截,使用 @WebFilter
注解进行配置。如:@WebFilter("拦截路径")
拦截路径有如下四种配置方式:
- 拦截具体的资源:/index.jsp:只有访问index.jsp时才会被拦截
- 目录拦截:/user/*:访问/user下的所有资源,都会被拦截
- 后缀名拦截:*.jsp:访问后缀名为jsp的资源,都会被拦截
- 拦截所有:/*:访问所有资源,都会被拦截
过滤器链
概述
过滤器链是指在一个Web应用,可以配置多个过滤器,这多个过滤器称为过滤器链。
如下图就是一个过滤器链,我们学习过滤器链主要是学习过滤器链执行的流程

上图中的过滤器链执行是按照以下流程执行:
- 执行
Filter1
的放行前逻辑代码 - 执行
Filter1
的放行代码 - 执行
Filter2
的放行前逻辑代码 - 执行
Filter2
的放行代码 - 访问到资源
- 执行
Filter2
的放行后逻辑代码 - 执行
Filter1
的放行后逻辑代码
以上流程串起来就像一条链子,故称之为过滤器链。
代码演示
- 我们在
web.filter
包下再新建一个FilterDemo2
类,并实现Filter接口,重写其所有方法
@WebFilter("/*")
public class FilterDemo2 implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("2.Filter...");
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("4.Filter...");
}
public void destroy() {
}
}
- 修改
FilterDemo1
类的doFilter
方法,将输出语句调成我们预期的结果,最终测试的时候,看看是否符合预期
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("1.Filter...");
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("5.Filter...");
}
- 修改
hello.jsp
,将输出语句改为预期结果
<body>
<h1>HELLO FILTER</h1>
<%
System.out.println("3.Filter...");
%>
</body>
- 重启服务器,访问
hello.jsp
页面,控制台输出如下,符合我们预期的结果
1.Filter…
2.Filter…
3.Filter…
4.Filter…
5.Filter…
问题
上面代码中为什么是先执行 FilterDemo
,后执行 FilterDemo2
呢?
- 我们现在使用的是注解配置Filter,而这种配置方式的优先级是按照过滤器类名(字符串)的自然排序。
- 比如有如下两个名称的过滤器 : BFilterDemo
和 AFilterDemo
。那一定是 AFilterDemo
过滤器先执行。
案例
需求
访问服务器资源时,需要先进行登录验证,如果没有登录,则自动跳转到登录页面
分析
要是搁以前,我们要实现该功能可能得在每一个资源里加入登陆状态校验的代码
但现在,只需要写一个 Filter
,在该过滤器中进行登陆状态校验即可。而在该 Filter
中逻辑如下:
- 判断访问的是否为登录之后才能看的资源
- 是:放行
- 不是:进行登录验证
- 判断用户是否登录:Session中是否有user对象
- 登录:放行
- 未登录:跳转至登录页面,并给出提示信息
代码实现
创建Filter
在 brand-demo
工程创建 com.itheima.web.filter
包,在该下创建名为 LoginFilter
的过滤器
@WebFilter("/*")
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
}
public void init(FilterConfig config) throws ServletException {
}
public void destroy() {
}
}
编写逻辑代码
在 doFilter()
方法中编写登陆状态校验的逻辑代码。
我们首先需要从 session
对象中获取用户信息,但是 ServletRequest
类型的 requset 对象没有获取 session 对象的方法,所以此时需要将 request对象强转成 HttpServletRequest
对象。
@WebFilter("/*")
public class LoginFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
//强转操作
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpSession session = httpRequest.getSession();
//获取是否有user对象
Object user = session.getAttribute("user");
//如果有user对象,那么就是已登录状态,直接放行
if (user != null) {
chain.doFilter(request, response);
//如果没有user对象,那就是未登录,不允许访问,给出错误信息并跳转到登录页面
} else {
httpRequest.setAttribute("login_msg", "您尚未登录");
httpRequest.getRequestDispatcher("/login.jsp").forward(request, response);
}
}
public void init(FilterConfig config) throws ServletException {
}
public void destroy() {
}
}
测试并抛出问题
重启服务器,访问register.jsp
注册页面,竟然访问不了了,也是直接跳转到了登录页面,而且如果你配置了css文件,css样式也显示不出来了,这是怎么回事呢?
问题分析及解决
因为我们配置的是对所有页面进行拦截,但现在需要对所有的登陆和注册相关的资源进行放行。
所以我们需要在判断session中是否包含用户信息之前,应该加上对登陆及注册相关资源放行的逻辑处理
HttpServletRequest httpRequest = (HttpServletRequest) request;
//在数组中存储注册和登录相关的资源路径
String[] urls = {"/login.jsp","/register.jsp","/checkCodeServlet","/registerServlet","/loginServlet"};
//获取当前的访问路径
String url = httpRequest.getRequestURL().toString();
//遍历数组
for (String u : urls) {
/*
判断当前访问的资源路径字符串是否包含要放行的的资源路径字符串
比如当前访问的资源路径是 /brand-demo/login.jsp
而字符串 /brand-demo/login.jsp 包含了字符串 /login.jsp ,所以这个字符串就需要放行
*/
if (url.contains(u)){
//找到了就放行
chain.doFilter(request,response);
return;
}
}
过滤器完整代码
@WebFilter("/*")
public class LoginFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String[] urls = {"/login.jsp","/register.jsp","/checkCodeServlet","/registerServlet","/loginServlet"};
String url = httpRequest.getRequestURL().toString();
for (String u : urls) {
if (url.contains(u)){
chain.doFilter(request,response);
return;
}
}
HttpSession session = httpRequest.getSession();
Object user = session.getAttribute("user");
if (user != null) {
chain.doFilter(request, response);
} else {
httpRequest.setAttribute("login_msg", "您尚未登录");
httpRequest.getRequestDispatcher("/login.jsp").forward(request, response);
}
}
public void init(FilterConfig config) throws ServletException {
}
public void destroy() {
}
}
Listener
概述
Listener 表示监听器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一。
监听器可以监听就是在
application
,session
,request
三个对象创建、销毁或者往其中添加修改删除属性时自动执行代码的功能组件。request 和 session 我们学习过。而
application
是ServletContext
类型的对象。ServletContext
代表整个web应用,在服务器启动的时候,tomcat会自动创建该对象。在服务器关闭时会自动销毁该对象。
分类
JavaWeb 提供了8个监听器:
监听器分类 | 监听器名称 | 作用 |
---|---|---|
servletContext监听 | servletContextListener | 用于对ServletContext对象进行监听(创建、销毁) |
ServletContextAttributeListener | 对ServletContext对象中属性的监听(增删改属性) | |
session监听 | HttpSessionListener | 对Session对象的整体状态的监听(创建、销毁) |
HttpSessionAttributeListener | 对Session对象中的属性监听(增删改属性) | |
HttpSessionBindingListener | 监听对象于Session的绑定和解除 | |
HttpsessionActivationListener | 对Session数据的钝化和活化的监听 | |
Request监听 | servletRequestListener | 对Request对象进行监听(创建、销毁) |
servletRequestAttributeListener | 对Request对象中属性的监听(增删改属性) |
这里面只有 ServletContextListener
这个监听器后期我们会接触到,ServletContextListener
是用来监听 ServletContext
对象的创建和销毁。
ServletContextListener
接口中有以下两个方法
void contextInitialized(ServletContextEvent sce)
:ServletContext
对象被创建了会自动执行的方法void contextDestroyed(ServletContextEvent sce)
:ServletContext
对象被销毁时会自动执行的方法
代码演示
我们只演示一下 ServletContextListener
监听器
- 定义一个类,实现
ServletContextListener
接口 - 重写所有的抽象方法
- 使用
@WebListener
进行配置
代码如下:
@WebListener
public class ContextLoaderListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
//加载资源
System.out.println("ContextLoaderListener...");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
//释放资源
}
}
启动服务器,就可以在控制台输出了 ContextLoaderListener...
,同时也说明了 ServletContext
对象在服务器启动的时候被创建了。
Ajax
AJAX
(Asynchronous JavaScript And XML):异步的 JavaScript 和 XML。
作用:
与服务器进行数据交换:通过AJAX可以给服务器发送请求,服务器将数据直接响应回给浏览器。
异步交互:可以在不重新加载整个页面的情况下,与服务器交换数据并更新部分网页的技术,如:搜索联想、用户名是否可用校验,等等…


Axios
Axios 对原生的AJAX进行封装,简化书写。
Axios官网是:https://www.axios-http.cn
基本使用
使用axios 发送请求,并获取响应结果
发送 get 请求
axios({ method:"get", url:"http://localhost:8080/ajax-demo1/ajaxDemo?username=zhangsan" }).then(function (resp){ alert(resp.data); })
发送 post 请求
axios({ method:"post", url:"http://localhost:8080/ajax-demo1/ajaxDemo", data:"username=zhangsan" }).then(function (resp){ alert(resp.data); });
axios()
是用来发送异步请求的,小括号中使用 js 对象传递请求相关的参数:
method
属性:用来设置请求方式的。取值为get
或者post
。url
属性:用来书写请求的资源路径。如果是get
请求,需要将请求参数拼接到路径的后面,格式为:url?参数名=参数值&参数名2=参数值2
。data
属性:作为请求体被发送的数据。也就是说如果是post
请求的话,数据需要作为data
属性的值。then()
需要传递一个匿名函数。我们将then()
中传递的匿名函数称为回调函数
,意思是该匿名函数在发送请求时不会被调用,而是在成功响应后调用的函数。而该回调函数中的resp
参数是对响应的数据进行封装的对象,通过resp.data
可以获取到响应的数据。
快速入门
后端实现
- 定义一个用于接收请求的servlet,代码如下:
@WebServlet("/ajaxServlet")
public class AjaxServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("get...");
//接收请求参数
String username = request.getParameter("username");
System.out.println(username);
//响应数据
response.getWriter().write("HELLO AXIOS");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("post...");
this.doGet(request, response);
}
}
前端实现
引入 js 文件
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
发送 ajax 请求
- get 请求
axios({ method: "get", url: "http://localhost:8080/ajax_demo/ajaxServlet?username=zhangsan" }).then(function (resp) { alert(resp.data); })
- post 请求
axios({ method: "post", url: "http://localhost:8080/ajax_demo/ajaxServlet", data: "username=zhangsan" }).then(function (resp) { alert(resp.data); })
整体页面代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
// axios({
// method: "get",
// url: "http://localhost:8080/ajax_demo/ajaxServlet?username=zhangsan"
// }).then(function (resp) {
// alert(resp.data);
// })
axios({
method: "post",
url: "http://localhost:8080/ajax_demo/ajaxServlet",
data: "username=zhangsan"
}).then(function (resp) {
alert(resp.data);
})
</script>
</body>
</html>
分别测试get请求和post请求,
- get请求会在页面上出现一个alert弹窗显示
HELLO AXIOS
,同时控制台输出
get…
zhangsan- post请求会在页面上出现一个alert弹窗显示
HELLO AXIOS
,同时控制台输出
post…
get…
zhangsan- get请求会在页面上出现一个alert弹窗显示
请求方法别名
为了方便起见, Axios 已经为所有支持的请求方法提供了别名。如下:
get
请求 :axios.get(url[,config])
delete
请求 :axios.delete(url[,config])
head
请求 :axios.head(url[,config])
options
请求 :axios.option(url[,config])
post
请求:axios.post(url[,data[,config])
put
请求:axios.put(url[,data[,config])
patch
请求:axios.patch(url[,data[,config])
而我们只关注 get
请求和 post
请求。
- 入门案例中的
get
请求代码可以改为如下:
axios.get("http://localhost:8080/ajax-demo/axiosServlet?username=zhangsan").then(function (resp) {
alert(resp.data);
});
- 入门案例中的
post
请求代码可以改为如下:
axios.post("http://localhost:8080/ajax-demo/axiosServlet","username=zhangsan").then(function (resp) {
alert(resp.data);
})
JSON
概述
概念:JavaScript Object Notation
。JavaScript 对象表示法.
如下是 JavaScript
对象的定义格式:
{
name:"zhangsan",
age:23,
city:"北京"
}
接下来我们再看看 JSON
的格式:
{
"name":"zhangsan",
"age":23,
"city":"北京"
}
通过上面 js 对象格式和 json 格式进行对比,发现两个格式特别像。只不过 js 对象中的属性名可以使用引号(可以是单引号,也可以是双引号);而 json
格式中的键要求必须使用双引号括起来,这是 json
格式的规定。json
格式的数据有什么作用呢?
作用:由于其语法格式简单,层次结构鲜明,现多用于作为数据载体
JSON基础语法
定义格式
JSON本质就是一个字符串,但是该字符串内容是有一定的格式要求的。 定义格式如下:
var 变量名 = {"key":value,"key":value,...};
JSON
串的键要求必须使用双引号括起来,而值根据要表示的类型确定。value 的数据类型分为如下
- 数字(整数或浮点数)
- 字符串(使用双引号括起来)
- 逻辑值(true或者false)
- 数组(在方括号中)
- 对象(在花括号中)
- null
代码演示
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
let jsonStr = '{"name":"zhangsan","age":18,"addr":["北京","上海","广州","深圳"]}';
alert(jsonStr);
</script>
</body>
</html>
通过浏览器打开,浏览器会有一个弹窗显示`
现在我们需要获取到该 JSON
串中的 name
属性值,应该怎么处理呢?
- 如果它是一个 js 对象,我们就可以通过
js对象.属性名
的方式来获取数据。JS 提供了一个对象JSON
,该对象有如下两个方法:parse(str)
:将 JSON串转换为 js 对象。使用方式是:var jsObject = JSON.parse(jsonStr);
stringify(obj)
:将 js 对象转换为 JSON 串。使用方式是:var jsonStr = JSON.stringify(jsObject)
- 代码演示
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
let jsonStr = '{"name":"zhangsan","age":18,"addr":["北京","上海","广州","深圳"]}';
alert(jsonStr);
//将json字符串转化为js对象
let jsonObj = JSON.parse(jsonStr);
alert(jsonObj);
alert(jsonObj.name);
alert(jsonObj.age);
alert(jsonObj.addr);
//将js对象转化为json字符串
let jsonStr2 = JSON.stringify(jsonObj);
alert(jsonStr2);
</script>
</body>
</html>
发送异步请求携带参数
后面我们使用 axios
发送请求时,如果要携带复杂的数据时都会以 JSON
格式进行传递
请求参数不可能由我们自己拼接字符串的,我们可以提前定义一个 js 对象,用来封装需要提交的参数,然后使用 JSON.stringify(js对象)
转换为 JSON
串,再将该 JSON
串作为 axios
的 data
属性值进行请求参数的提交。如下:
var jsObject = {name:"张三"};
axios({
method:"post",
url:"http://localhost:8080/ajax-demo/axiosServlet",
data: JSON.stringify(jsObject)
}).then(function (resp) {
alert(resp.data);
})
而 axios
是一个很强大的工具。我们只需要将需要提交的参数封装成 js 对象,并将该 js 对象作为 axios
的 data
属性值进行,它会自动将 js 对象转换为 JSON
串进行提交。如下:
var jsObject = {name:"张三"};
axios({
method:"post",
url:"http://localhost:8080/ajax-demo/axiosServlet",
data:jsObject //这里 axios 会将该js对象转换为 json 串的
}).then(function (resp) {
alert(resp.data);
})
注意:
js 提供的 JSON
对象我们只需要了解一下即可。因为 axios
会自动对 js 对象和 JSON
串进行转换。
发送异步请求时,如果请求参数是 JSON
格式,那请求方式必须是 POST
。因为 JSON
串需要放在请求体中。
JSON串与Java对象的相互转换
学习完 json 后,接下来聊聊 json 的作用。以后我们会以 json 格式的数据进行前后端交互。前端发送请求时,如果是复杂的数据就会以 json 提交给后端;而后端如果需要响应一些复杂的数据时,也需要以 json 格式将数据响应回给浏览器。
在后端我们就需要重点学习以下两部分操作:
- 请求数据:JSON字符串转为Java对象
- 响应数据:Java对象转为JSON字符串
阿里提供的一套 API Fastjson
,可以帮我们实现上面两部分操作。
Fastjson概述
Fastjson
是阿里巴巴提供的一个Java语言编写的高性能功能完善的 JSON
库,是目前Java语言中最快的 JSON
库,可以实现 Java
对象和 JSON
字符串的相互转换。
Fastjson使用
Fastjson
使用也是比较简单的,分为以下三步完成
导入坐标
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.62</version> </dependency>
Java对象转JSON
String jsonStr = JSON.toJSONString(obj);
将 Java 对象转换为 JSON 串,只需要使用
Fastjson
提供的
JSON
类中的toJSONString()
静态方法即可。JSON字符串转Java对象
User user = JSON.parseObject(jsonStr, User.class);
将 json 转换为 Java 对象,只需要使用
Fastjson
提供的JSON
类中的parseObject()
静态方法即可。
代码演示
- 先创建一个User类
package com.blog.web.pojo;
public class User {
private Integer id;
private String username;
private String password;
public User() {
}
public User(Integer id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
- 测试方法
import com.alibaba.fastjson.JSON;
import com.blog.web.pojo.User;
public class FastjsonDemo {
public static void main(String[] args) {
//1. 将Java对象转为JSON字符串
User user = new User();
user.setId(1);
user.setUsername("zhangsan");
user.setPassword("asd123");
String jsonString = JSON.toJSONString(user);
System.out.println(jsonString);
//2. 将JSON字符串转为Java对象
User u = JSON.parseObject("{\"id\":1,\"password\":\"asd123\",\"username\":\"zhangsan\"}", User.class);
System.out.println(u);
}
}
- 得到输出结果如下
{“id”:1,“password”:“asd123”,“username”:“zhangsan”}
User
VUE
概述
Vue是一套前端框架,用于简化JavaScript中的DOM操作,简化书写。
在之前我们也学习过后端的框架MyBatis,MyBatis是用来简化JDBC代码编写的;而Vue是前端框架,简化JavaScript代码编写的。
在上篇文章中,我们做了一个综合性的案例,里面进行了大量的DOM操作,如下
// 获取表单数据
let brandName = document.getElementById("brandName").value;
// 设置数据
formData.brandName = brandName;
// 获取表单数据
let companyName = document.getElementById("companyName").value;
// 设置数据
formData.companyName = companyName;
// 获取表单数据
let ordered = document.getElementById("ordered").value;
// 设置数据
formData.ordered = ordered;
// 获取表单数据
let description = document.getElementById("description").value;
// 设置数据
formData.description = description;
学习了Vue之后,这部分代码我们就不需要再写了,那么Vue是如何简化DOM书写呢?
基于MVVM(Model-View-ViewModel)思想,实现数据的双向绑定,将变成的关注点放在数据上。之前我们是将关注点放在了DOM操作上;而要了解MVVM思想,就得先聊聊MVC思想,下面是MVC思想的图解

M 就是数据,而 V 是页面上展示的内容,C是我们的JS代码,如下是我们之前写的代码
let brands = resp.data;
for (let i = 0; i < brands.length; i++) {
let brand = brands[i]; //这行就是我们的数据
//下面这些就是到时候要展示到浏览器上的视图
tableData += "<tr align=\"center\">\n" +
" <td>" + (i + 1) + "</td>\n" +
" <td>" + brand.brandName + "</td>\n" +
" <td>" + brand.companyName + "</td>\n" +
" <td>" + brand.ordered + "</td>\n" +
" <td>" + brand.description + "</td>\n" +
" <td>" + (brand.status == 1 ? "启用" : "禁用") + "</td>\n" +
"<td><a href=\"/brand_demo/selectByIdServlet?id=" + brand.id + "\">修改</a> <a href=\"/brand_demo/deleteServlet?id=" + brand.id + "\">删除</a></td>" +
" </tr>"
}
MVC
思想是没法进行双向绑定的。双向绑定是指当数据模型数据发生变化时,页面展示的会随之发生变化,而如果表单数据发生变化,绑定的模型数据也随之发生变化。接下来我们聊聊 MVVM
思想,如下图是三个组件图解

图中的 Model
就是我们的数据,View
是视图,也就是页面标签,用户可以通过浏览器看到的内容;Model
和 View
是通过 ViewModel
对象进行双向绑定的,而 ViewModel
对象是 Vue
提供的。接下来让我们看看双向绑定的效果,输入框绑定了 username
模型数据,而在页面上也使用插值表达式绑定了 username
模型数据
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<input v-model="username">
<!--插值表达式-->
{{username}}
</div>
<script src="js/vue.js"></script>
<!--创建Vue核心对象-->
<script>
new Vue({
el: "#app",
data() {
return {
username: ""
}
}
})
</script>
</body>
</html>
当我们访问该页面时,在输入框中输入内容,而输入框后面随之实时的展示我们输入的内容,这就是双向绑定的效果。
快速入门
Vue 使用起来是比较简单的,总共分为如下三步:
新建 HTML 页面,引入 Vue.js文件
<script src="js/vue.js"></script>
在JS代码区域,创建Vue核心对象,进行数据绑定
new Vue({ el: "#app", data() { return { username: "" } } });
创建 Vue 对象时,需要传递一个 js 对象,而该对象中需要如下属性:
el
: 用来指定哪儿些标签受 Vue 管理。 该属性取值#app
中的app
需要是受管理的标签的id属性值data
:用来定义数据模型methods
:用来定义函数。这个我们在后面就会用到
编写视图
<div id="app"> <input name="username" v-model="username" > {{username}} </div>
{{}}
是 Vue 中定义的插值表达式
,在里面写数据模型,到时候会将该模型的数据值展示在这个位置。
整体代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<input v-model="username">
<!--插值表达式-->
{{username}}
</div>
<script src="js/vue.js"></script>
<script>
//1. 创建Vue核心对象
new Vue({
el:"#app",
data(){ // data() 是 ECMAScript 6 版本的新的写法
return {
username:""
}
}
/*data: function () {
return {
username:""
}
}*/
});
</script>
</body>
</html>
Vue 指令
指令:HTML 标签上带有 v-
前缀的特殊属性,不同指令具有不同含义。例如:v-if
,v-for
…
常用的指令有
指令 | 作用 |
---|---|
v-bind | 为HTML标签绑定属性值,如设置 href , css样式等 |
v-model | 在表单元素上创建双向数据绑定 |
v-on | 为HTML标签绑定事件 |
v-if | 条件性的渲染某元素,判定为true时渲染,否则不渲染 |
v-else | 条件性的渲染某元素,判定为true时渲染,否则不渲染 |
v-else-if | 条件性的渲染某元素,判定为true时渲染,否则不渲染 |
v-show | 根据条件展示某元素,区别在于切换的是display属性的值 |
v-for | 列表渲染,遍历容器的元素或者对象的属性 |
v-bind&v-model指令
v-bind
该指令可以给标签原有属性绑定模型数据。这样模型数据发生变化,标签属性值也随之发生变化
例如:
<a v-bind:href="url">百度一下</a>
上面的
v-bind:"
可以简化写成:
,如下:<!-- v-bind 可以省略 --> <a :href="url">百度一下</a>
v-model
该指令可以给表单项标签绑定模型数据。这样就能实现双向绑定效果。例如:
HTML <input name="username" v-model="username">
代码演示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<input v-model="url"> <!--给表单项绑定模型数据-->
<a :href="url">`链接`</a> <!--点击链接,可以根据我们输入的url来访问不同的网站-->
</div>
<script src="js/vue.js"></script>
<script>
new Vue({
el: "#app",
data() {
return {
username: "",
url:""
}
}
})
</script>
</body>
</html>
v-on指令
我们在页面定义一个按钮,并给该按钮使用 v-on
指令绑定单击事件,html代码如下
<input type="button" value="一个按钮" v-on:click="show()">
而使用 v-on
时还可以使用简化的写法,将 v-on:
替换成 @
,html代码如下
<input type="button" value="一个按钮" @click="show()">
上面代码绑定的 show()
需要在 Vue 对象中的 methods
属性中定义出来
new Vue({
el: "#app",
methods: {
show() {
alert("我被点了")
}
}
})
注意:
v-on:
后面的事件名称是之前原生事件属性名去掉on。
- 单击事件 : 事件属性名是 onclick,而在vue中使用是
v-on:click
- 失去焦点事件:事件属性名是 onblur,而在vue中使用时
v-on:blur
- 单击事件 : 事件属性名是 onclick,而在vue中使用是
整体页面代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<input type="button" value="按钮" v-on:click="show()">
</div>
<script src="js/vue.js"></script>
<script>
new Vue({
el: "#app",
methods: {
show() {
alert("我被点了")
}
}
})
</script>
</body>
</html>
条件判断指令
在 Vue中定义一个 count
的数据模型,如下
new Vue({
el: "#app",
data() {
return {
count: ""
}
}
})
现在要实现,当 count
模型的数据是1时,在页面上展示 div1
内容;当 count
模型的数据是2时,在页面上展示 div2
内容;count
模型数据是其他值时,在页面上展示 div3
。这里为了动态改变模型数据 count
的值,再定义一个输入框绑定 count
模型数据。html 代码如下:
<div id="app">
<div v-if="count==1">div1</div>
<div v-else-if="count==2">div2</div>
<div v-else>div3</div>
<input v-model="count">
</div>
整体代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<div v-if="count==1">div1</div>
<div v-else-if="count==2">div2</div>
<div v-else>div3</div>
<input v-model="count">
</div>
<script src="js/vue.js"></script>
<script>
new Vue({
el: "#app",
data() {
return {
count: ""
}
}
})
</script>
</body>
</html>
通过浏览器打开页面并在输入框输入不同的值,会显示不同的div
v-show
和 v-if
效果一样,但实现原理不一样,通过查看源码发现,v-show
不展示的原理是给对应的标签添加 display
css属性,并将该属性值设置为 none
,而v-if
是直接没有div标签,只有符合条件的时候,源码中才会出现对应的div标签
<div style="display: none;">div4</div>
v-for指令
这个指令看到名字就知道是用来遍历的,该指令使用的格式如下:
<标签 v-for="变量名 in 集合模型数据">
{{变量名}}
</标签>
注意:需要循环哪个标签,v-for
指令就写在哪个标签上。
如果在页面需要使用到集合模型数据的索引,就需要使用如下格式:
<标签 v-for="(变量名,索引变量) in 集合模型数据">
<!--索引变量是从0开始,所以要表示序号的话,需要手动的加1-->
{{索引变量 + 1}} {{变量名}}
</标签>
代码演示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<div v-for="addr in addrs">
{{addr}}<br>
</div>
<div v-for="(addr,i) in addrs">
{{i + 1}}--{{addr}}<br>
</div>
</div>
<script src="js/vue.js"></script>
<script>
new Vue({
el: "#app",
data() {
return {
addrs: ["北京", "上海", "成都"]
}
}
})
</script>
</body>
</html>
浏览器上展示的内容
北京
上海
成都
1–北京
2–上海
3–成都
生命周期
生命周期的八个阶段:每触发一个生命周期事件,会自动执行一个生命周期方法,这些生命周期方法也被称为钩子方法。
状态 | 阶段周期 |
---|---|
beforeCreate | 创建前 |
created | 创建后 |
beforeMount | 载入前 |
mounted | 挂载完成 |
beforeUpdate | 更新前 |
updated | 更新后 |
beforeDestroy | 销毁前 |
destroyed | 销毁后 |
mounted`:挂载完成,Vue初始化成功,HTML页面渲染成功。而以后我们会在该方法中`发送异步请求,加载数据。
案例
需求
使用 Vue 简化我们在上一篇文章学完Ajax后做的品牌列表数据查询和添加功能
此案例只是使用 Vue 对前端代码进行优化,后端代码无需修改。
查询所有功能
- 在 brand.html 页面引入 vue和Axios 的js文件
<script src="js/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
- 创建 Vue 对象
- 在 Vue 对象中定义模型数据
- 在mounted函数中发送异步请求(也就是页面加载完成之后),并将响应的数据赋值给数据模型
new Vue({
el: "#app",
data() {
return {
brands: []
}
},
mounted() {
/*_this就是个中介,因为在axios中不能直接获取到vue中的brands,
所以需要在mounted内,axios外,用_this这个中介来将数据赋给brands
*/
var _this = this;
axios({
method: "get",
url: "http://localhost:8080/brand_demo/selectAllServlet"
}).then(function (resp) {
_this.brands = resp.data;
})
}
})
- 修改视图
- 定义
<div id="app"></div>
,指定该div
标签受 Vue 管理 - 在表格中的数据行上使用
v-for
指令遍历
- 定义
<tr v-for="(brand,i) in brands" align="center">
<td>{{i + 1}}</td>
<td>{{brand.brandName}}</td>
<td>{{brand.companyName}}</td>
<td>{{brand.ordered}}</td>
<td>{{brand.description}}</td>
<td>{{brand.status == 1 ? "启用" : "禁用"}}</td>
<td><a href="#">修改</a>
<a href="#">删除</a></td>
</tr>
- 整体代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>欢迎你</h1>
<input type="button" value="新增" id="add"><br>
<hr>
<div id="app">
<table border="1" cellspacing="0" width="1200">
<tr>
<th>序号</th>
<th>品牌名称</th>
<th>企业名称</th>
<th>排序</th>
<th>品牌介绍</th>
<th>状态</th>
<th>操作</th>
</tr>
<tr v-for="(brand,i) in brands" align="center">
<td>{{i + 1}}</td>
<td>{{brand.brandName}}</td>
<td>{{brand.companyName}}</td>
<td>{{brand.ordered}}</td>
<td>{{brand.description}}</td>
<td>{{brand.status == 1 ? "启用" : "禁用"}}</td>
<td><a href="#">修改</a>
<a href="#">删除</a></td>
</tr>
</table>
</div>
<script src="js/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
new Vue({
el: "#app",
data() {
return {
brands: []
}
},
mounted() {
var _this = this;
axios({
method: "get",
url: "http://localhost:8080/brand_demo/selectAllServlet"
}).then(function (resp) {
_this.brands = resp.data;
})
}
})
</script>
</body>
</html>
添加功能
- 在 vueAddBrand.html 页面引入 vue和axios 的js文件
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="js/vue.js"></script>
创建 Vue 对象
- 在 Vue 对象中定义模型数据
brand
- 定义一个
submitForm()
函数,用于给提交
按钮提供绑定的函数 - 在
submitForm()
函数中发送 ajax 请求,并将模型数据brand
作为参数进行传递
new Vue({ el: "#app", data(){ return { brand:{} } }, methods:{ submitForm(){ // 发送ajax请求,添加 var _this = this; axios({ method:"post", url:"http://localhost:8080/brand_demo/addServlet", data:_this.brand }).then(function (resp) { // 判断响应数据是否为 success if(resp.data == "success"){ location.href = "http://localhost:8080/brand_demo/vueBrand.html"; } }) } } })
- 在 Vue 对象中定义模型数据
修改视图
- 定义
<div id="app"></div>
,指定该div
标签受 Vue 管理 - 将表单的所有的内容复制到
div
标签中 - 给每一个表单项标签绑定模型数据。最后这些数据要被封装到
brand
对象中
<div id="app"> <h3>添加品牌</h3> <form action="" method="post"> 品牌名称:<input id="brandName" v-model="brand.brandName" name="brandName"><br> 企业名称:<input id="companyName" v-model="brand.companyName" name="companyName"><br> 排序:<input id="ordered" v-model="brand.ordered" name="ordered"><br> 描述信息:<textarea rows="5" cols="20" id="description" v-model="brand.description" name="description"></textarea><br> 状态: <input type="radio" name="status" v-model="brand.status" value="0">禁用 <input type="radio" name="status" v-model="brand.status" value="1">启用<br> <input type="button" id="btn" @click="submitForm" value="提交"> </form> </div>
- 定义
整体页面代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>添加品牌</title>
</head>
<body>
<div id="app">
<h3>添加品牌</h3>
<form action="" method="post">
品牌名称:<input id="brandName" v-model="brand.brandName" name="brandName"><br>
企业名称:<input id="companyName" v-model="brand.companyName" name="companyName"><br>
排序:<input id="ordered" v-model="brand.ordered" name="ordered"><br>
描述信息:<textarea rows="5" cols="20" id="description" v-model="brand.description" name="description"></textarea><br>
状态:
<input type="radio" name="status" v-model="brand.status" value="0">禁用
<input type="radio" name="status" v-model="brand.status" value="1">启用<br>
<input type="button" id="btn" @click="submitForm" value="提交">
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="js/vue.js"></script>
<script>
new Vue({
el: "#app",
data(){
return {
brand:{}
}
},
methods:{
submitForm(){
// 发送ajax请求,添加
var _this = this;
axios({
method:"post",
url:"http://localhost:8080/brand_demo/addServlet",
data:_this.brand
}).then(function (resp) {
// 判断响应数据是否为 success
if(resp.data == "success"){
location.href = "http://localhost:8080/brand_demo/vueBrand.html";
}
})
}
}
})
</script>
</body>
</html>
Element
- Element:是饿了么公司前端开发团队提供的一套基于 Vue 的网站组件库,用于快速构建网页。
- Element 提供了很多组件(组成网页的部件)供我们使用。例如 超链接、按钮、图片、表格等等~
- 我们学习 Element 其实就是学习怎么从官网拷贝组件到我们自己的页面并进行修改,官网网址是 https://element.eleme.cn/#/zh-CN
快速入门
- 引入Element 的css、js文件 和 Vue.js
- 创建Vue核心对象,Element是基于Vue的,所以使用Element时必须创建Vue对象
<script>
new Vue({
el: "#app"
})
</script>
官网复制Element组件代码,然后随便改改就变成自己的了,本文就不过多赘述了,下篇文章会有一个具体的示例。
JavaWeb综合案例
JavaWeb---综合案例 | Kyle's Blog (cyborg2077.github.io)

pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>JavaWebFinal</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!-- mybatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.46</version> </dependency> <!--servlet--> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <!--fastjson--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.62</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> <configuration> <path>/</path> <port>7777</port> </configuration> </plugin> </plugins> </build> </project>
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!--起别名--> <typeAliases> <package name="pojo"/> </typeAliases> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql:///learn?useSSL=false&useServerPrepStmts=true"/> <property name="username" value="cdd"/> <property name="password" value="1"/> </dataSource> </environment> </environments> <mappers> <!--扫描mapper--> <package name="mapper"/> </mappers> </configuration>
Brand类
package pojo; public class Brand { // id 主键 private Integer id; // 品牌名称 private String brandName; // 企业名称 private String companyName; // 排序字段 private Integer ordered; // 描述信息 private String description; // 状态:0:禁用 1:启用 private Integer status; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getBrandName() { return brandName; } public void setBrandName(String brandName) { this.brandName = brandName; } public String getCompanyName() { return companyName; } public void setCompanyName(String companyName) { this.companyName = companyName; } public Integer getOrdered() { return ordered; } public void setOrdered(Integer ordered) { this.ordered = ordered; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Integer getStatus() { return status; } //逻辑视图 public String getStatusStr(){ if (status == null){ return "未知"; } return status == 0 ? "禁用":"启用"; } public void setStatus(Integer status) { this.status = status; } @Override public String toString() { return "Brand{" + "id=" + id + ", brandName='" + brandName + '\'' + ", companyName='" + companyName + '\'' + ", ordered=" + ordered + ", description='" + description + '\'' + ", status=" + status + '}'; } }
BrandService
package service; import mapper.BrandMapper; import pojo.Brand; import java.util.List; public interface BrandService{ List<Brand> selectAll(); int add(Brand brand); }
BrandServiceImpl
package service.impl; import mapper.BrandMapper; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import pojo.Brand; import service.BrandService; import util.SqlSessionFactoryUtils; import java.util.List; public class BrandServiceImpl implements BrandService { //1. 创建SqlSessionFactory工厂对象 SqlSessionFactory sqlSessionFactory = SqlSessionFactoryUtils.getSqlSessionFactory(); @Override public List<Brand> selectAll() { //2. 获取SqlSession对象 SqlSession sqlSession = sqlSessionFactory.openSession(); //3. 获取BrandMapper BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class); //4. 调用方法 List<Brand> brands = brandMapper.selectAll(); //5. 关闭资源 sqlSession.close(); return brands; } @Override public int add(Brand brand) { SqlSession sqlSession = sqlSessionFactory.openSession(); //3. 获取BrandMapper BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class); //4. 调用方法 Integer res = brandMapper.add(brand); sqlSession.commit(); //5. 关闭资源 sqlSession.close(); return res; } }
BrandMapper
package mapper; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.ResultMap; import org.apache.ibatis.annotations.Select; import pojo.Brand; import java.util.List; @Mapper public interface BrandMapper { @Select("select * from tb_brand") @ResultMap("brandResultMap") List<Brand> selectAll(); @Insert("insert into tb_brand values (null, #{brandName}, #{companyName}, #{ordered}, #{description}, #{status})") @ResultMap("brandResultMap") Integer add(Brand brand); }
BrandMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="mapper.BrandMapper"> <resultMap id="brandResultMap" type="brand"> <result property="brandName" column="brand_name" /> <result property="companyName" column="company_name" /> </resultMap> </mapper>
SqlSessionFactoryUtils
package util; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream; public class SqlSessionFactoryUtils { private static SqlSessionFactory sqlSessionFactory; static { //静态代码块会随着类的加载而自动执行,且只执行一次 try { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); } catch (IOException e) { e.printStackTrace(); } } public static SqlSessionFactory getSqlSessionFactory(){ return sqlSessionFactory; } }
SelectAllServlet
package web.servlet; import com.alibaba.fastjson.JSON; import pojo.Brand; import service.BrandService; import service.impl.BrandServiceImpl; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; @WebServlet("/selectAllServlet") public class SelectAllServlet extends HttpServlet { private BrandService brandService = new BrandServiceImpl(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //1. 调用方法,返回结果 List<Brand> brands = brandService.selectAll(); //2. 将返回的结果转换为json数据 String jsonString = JSON.toJSONString(brands); //告知浏览器响应的数据是什么, 告知浏览器使用什么字符集进行解码 resp.setContentType("text/json;charset=utf-8"); //3. 响应json数据 resp.getWriter().write(jsonString); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { this.doGet(req, resp); } }
AddServlet
package web.servlet; import com.alibaba.fastjson.JSON; import pojo.Brand; import service.BrandService; import service.impl.BrandServiceImpl; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; @WebServlet("/addServlet") public class AddServlet extends HttpServlet { private BrandService brandService = new BrandServiceImpl(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //解决乱码问题,一定要把它放在第一行 req.setCharacterEncoding("utf-8"); //获取输入流 BufferedReader br = req.getReader(); //读取数据 String params = br.readLine(); //将json格式的数据转换为一个Brand对象 Brand brand = JSON.parseObject(params, Brand.class); System.out.println(brand); Integer res = brandService.add(brand); if (res > 0) { //如果能顺利执行到此行,则说明添加成功,给浏览器响应一个success字符串表示成功 resp.getWriter().write("add success"); } else { resp.getWriter().write("add fail"); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { this.doGet(req, resp); } }
servlet优化
问题导入
Web 层的 Servlet 个数太多了,不利于管理和编写。
每一个功能都需要定义一个 servlet
,一个模块需要实现增删改查功能,就需要4个 servlet
,模块一多就会造成servlet
泛滥。此时我们就想 servlet
能不能像 service
一样,一个模块只定义一个 servlet
,而每一个功能只需要在该 servlet
中定义对应的方法。例如下面代码:
@WebServlet("/brand/*")
public class BrandServlet {
//查询所有
public void selectAll(...) {}
//添加数据
public void add(...) {}
//修改数据
public void update(...) {}
//删除删除
public void delete(...) {}
}
而我们知道发送请求 servlet
,tomcat
会自动的调用 service()
方法,HttpServlet的service()方法如下,当我们访问该 servlet
时会根据请求方式将请求分发给 doGet()
或者 doPost()
等方法
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod();
long lastModified;
if (method.equals("GET")) {
lastModified = this.getLastModified(req);
if (lastModified == -1L) {
this.doGet(req, resp);
} else {
long ifModifiedSince = req.getDateHeader("If-Modified-Since");
if (ifModifiedSince < lastModified) {
this.maybeSetLastModified(resp, lastModified);
this.doGet(req, resp);
} else {
resp.setStatus(304);
}
}
} else if (method.equals("HEAD")) {
lastModified = this.getLastModified(req);
this.maybeSetLastModified(resp, lastModified);
this.doHead(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {
this.doPut(req, resp);
} else if (method.equals("DELETE")) {
this.doDelete(req, resp);
} else if (method.equals("OPTIONS")) {
this.doOptions(req, resp);
} else if (method.equals("TRACE")) {
this.doTrace(req, resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[]{method};
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(501, errMsg);
}
}
那么我们也可以仿照这样请求分发的思想,在 service()
方法中根据具体的操作调用对应的方法,如:查询所有就调用 selectAll()
方法,添加企业信息就调用 add()
方法。
为了做到通用,我们定义一个通用的 servlet
类,再定义其他的 servlet
就不用继承 HttpServlet
,而是继承我们自定义的 BaseServlet
,在BaseServlet
中调用具体 servlet
(如BrandServlet
)中的对应方法。
public class BaseServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
BrandServlet定义修改如下
public class BrandServlet extends BaseServlet{
//用户实现分页查询
public void selectAll() {}
//添加企业信息
public void add() {}
//修改企业信息
public void update() {}
//删除企业信息
public void delete() {}
}
那么如何在 BaseServlet
中调用对应的方法呢?比如查询所有就调用 selectAll()
方法。
可以规定在发送请求时,请求资源的二级路径(/brandServlet/selectAll)和需要调用的方法名相同,如:
查询所有数据的路径以后就需要写成: http://localhost:7777/brand/selectAll
添加数据的路径以后就需要写成: http://localhost:7777/brand/add
修改数据的路径以后就需要写成: http://localhost:7777/brand/update
删除数据的路径以后就需要写成: http://localhost:7777/brand/delete
这样的话,在 BaseServlet
中就需要获取到资源的二级路径作为方法名,然后调用该方法
public class BaseServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取请求路径
String requestURI = req.getRequestURI();
//找到最后一个斜杠,它后面就是我们要的方法名
int index = requestURI.lastIndexOf('/');
//取子串就是斜杠后面的字符串
String methodName = requestURI.substring(index + 1);
//获取当前对象的字节码文件
Class<? extends BaseServlet> clazz = this.getClass();
try {
//根据前面的方法名,获取方法
Method method = clazz.getMethod(methodName,...);
//执行方法
method.invoke(this,...)
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
通过上面代码发现根据方法名获取对应方法的 Method
对象时需要指定方法参数的字节码对象。解决这个问题,可以将方法的参数类型规定死,而方法中可能需要用到 request
对象和 response
对象,所以指定方法的参数为 HttpServletRequest
和 HttpServletResponse
,那么 BrandServlet
代码就可以改进为:
public class BrandServlet extends BaseServlet{
//用户实现分页查询
public void selectAll(HttpServletRequest req, HttpServletResponse resp) {}
//添加企业信息
public void add(HttpServletRequest req, HttpServletResponse resp) {}
//修改企业信息
public void update(HttpServletRequest req, HttpServletResponse resp) {}
//删除企业信息
public void delete(HttpServletRequest req, HttpServletResponse resp) {}
}
BaseServlet代码可以改进为:
public class BaseServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取请求路径
String uri = req.getRequestURI(); // 例如路径为:/brand/selectAll
// 找到最后一个 / ,以此来划分方法名
int index = uri.lastIndexOf('/');
// 获取到资源的二级路径 selectAll
String methodName = uri.substring(index + 1);
//获取BrandServlet /UserServlet 字节码对象 Class
Class<? extends BaseServlet> cls = this.getClass();
//获取根据上面获取的方法名,来Method对象
try {
Method method = cls.getMethod(methodName, HttpServletRequest.class, HttpServletResponse.class);
//执行方法
method.invoke(this,req,resp);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
代码优化
后端优化
定义了 BaseServlet
后,针对品牌模块我们定义一个 BrandServlet
的 Servlet,并使其继承 BaseServlet
。在BrandServlet
中定义 以下功能的方法:
查询所有
功能:方法名声明为selectAll
,并将之前的SelectAllServlet
中的逻辑代码拷贝到该方法中添加数据
功能:方法名声明为add
,并将之前的AddServlet
中的逻辑代码拷贝到该方法中
具体代码如下:
@WebServlet("/brand/*")
public class BrandServlet extends BaseServlet{
private BrandService brandService = new BrandServiceImpl();
public void selectAll(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 调用service查询
List<Brand> brands = brandService.selectAll();
//2. 转为JSON
String jsonString = JSON.toJSONString(brands);
//3. 写数据
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(jsonString);
}
public void add(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//处理乱码问题
request.setCharacterEncoding("utf-8");
//1. 获取输入流,接收品牌数据json字符串
BufferedReader br = request.getReader();
String params = br.readLine();
//将json字符串转为Brand对象
Brand brand = JSON.parseObject(params, Brand.class);
//2. 调用service添加
Integer res = brandService.add(brand);
//3. 响应成功的标识
if (res > 0) {
resp.getWriter().write("add success");
}
else {
resp.getWriter().write("add fail");
}
}
}
前端优化
页面中之前发送的请求的路径都需要进行修改,selectAll()
和addBrand()
函数中发送异步请求的 url
应该改为http://localhost:7777/brand/selectAll
和http://localhost:7777/brand/add
selectAll(){
var _this = this;
axios({
method:"get",
url:"http://localhost:7777/brand/selectAll"
}).then(function (resp){
_this.tableData = resp.data;
})
},
addBrand() {
var _this = this;
axios({
method:"post",
url:"http://localhost:7777/brand/add",
data:_this.brand
}).then(function (resp){
if(resp.data == "add success"){
_this.dialogVisible = false
_this.selectAll();
_this.$message({
message: '添加成功',
type: 'success'
});
}
})
批量删除
点击多条数据前的复选框就意味着要删除这些数据,而点击了 批量删除
按钮后,需要让用户确认一下,因为有可能是用户误操作的,当用户确定后需要给后端发送请求并携带者需要删除数据的多个id值,前端发送请求时需要将要删除的多个id值以json格式提交给后端
后端实现
mapper.BrandMapper
- 接口方法声明如下
/**
* 批量删除
*
* @param ids
*/
Integer deleteByIds(@Param("ids") List<Integer> ids);
- 在
BrandMapper.xml
映射配置文件中添加 statement,需要用到动态sql的知识,在前面的文章已经详细说过了
<delete id="deleteByIds" parameterType="java.util.List">
DELETE FROM tb_brand
WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
service方法实现
- 在
BrandService
接口中定义deleteByIds()
批量删除的业务逻辑方法
/**
* 批量删除
* @param ids
*/
Integer deleteByIds(List<Integer> ids);
- 在
BrandServiceImpl
类中重写deleteByIds()
方法,并进行业务逻辑实现
@Override
public Integer deleteByIds(List<Integer> ids)
{
SqlSession sqlSession = sqlSessionFactory.openSession();
//3. 获取BrandMapper
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 调用方法
Integer res = brandMapper.deleteByIds(ids);
sqlSession.commit();
//5. 关闭资源
sqlSession.close();
return res;
}
servlet方法实现
- 在
BrandServlet
类中定义deleteByIds()
方法。而该方法的逻辑如下:- 接收页面提交的数据。页面到时候提交的数据是 json 格式的数据,所以此处需要使用输入流读取数据
- 将接收到的数据转换为
int[]
数组 - 调用 service 的
deleteByIds()
方法进行批量删除的业务逻辑处理 - 给浏览器响应添加成功的标识,这里直接给浏览器响应
success
字符串表示成功
- servlet 中
deleteByIds()
方法代码实现如下:
/**
* 批量删除
*
* @param request
* @param response
* @throws ServletException
* @throws IOException
*/
public void deleteByIds(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//处理乱码问题
request.setCharacterEncoding("utf-8");
//1. 获取输入流,接收品牌数据json字符串
BufferedReader br = request.getReader();
String params = br.readLine();
//将json字符串转为List<Integer>数组
List<Integer> ids = JSON.parseObject(params, List.class);
//2. 调用service添加
int res = brandService.deleteByIds(ids);
if(res > 0){
//3. 响应成功的标识
response.getWriter().write("delete success");
}
else {
response.getWriter().write("delete fail");
}
}
前端实现
- 先去ElementUI官网看看复选框都有什么属性,发现表单绑定了一个事件
@selection-change="handleSelectionChange
,
handleSelectionChange(val) {
this.multipleSelection = val;
}
}
})
</script>
</body>
</html>
<!--表格展示-->
<template>
<el-table
:data="tableData"
style="width: 100%"
@selection-change="handleSelectionChange"
:row-class-name="tableRowClassName">
- 该事件是当选择项发生变化时会触发。该事件绑定了
handleSelectionChange
函数,而该函数有一个参数val
,该参数是获取选中行的数据,选中多行也就是相当于选中了一个brand数组,我们可以通过遍历brand数组来获取其id,然后使用 axios 发送异步请求并经刚刚获取到的存储所有的 id 数组作为请求参数,那么现在就分析完毕了,开始敲代码 - 给批量删除绑定一个点击事件,并绑定触发时的调用函数
deleteByIds
<!--批量删除和修改的按钮-->
<!--这里绑定了一个点击事件,当点击时,将dialogVisible设为true,
然后我们就可以看到对话框了,所以我们要给新增按钮绑定这个点击事件-->
<el-row>
<el-button type="danger" @click="deleteByIds" plain>批量删除</el-button>
<el-button type="primary" @click="dialogVisible = true" plain>新增</el-button>
</el-row>
我们要在
deleteByIds
函数中遍历选中的brand数组,也就是遍历this.multipleSelection
,获取brand.id
,将其加入到id数组,所以我们先在Vue对象的data中添加selectedIds: []
由于删除操作是一个危险的操作,所以在删除之前,我们需要让用户确认一下是否删除,要完成这个需求,我们继续去ElementUI中找个组件,MessageBox弹框
<template>
<el-button type="text" @click="open">点击打开 Message Box</el-button>
</template>
<script>
export default {
methods: {
open() {
//这里的文案也可以改一下
this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
//当点击确定,就执行这里,将我们刚刚写的删除操作放到这里就可以了
this.$message({
type: 'success',
message: '删除成功!'
});
}).catch(() => {
//点击取消就不用管了
this.$message({
type: 'info',
message: '已取消删除'
});
});
}
}
}
</script>
- 直接把这个open函数的函数体搬走就可以,修改完成的
deleteByIds
函数代码如下
deleteByIds() {
this.$confirm('此操作将删除数据, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
for (let i = 0; i < this.multipleSelection.length; i++) {
console.log(this.multipleSelection[i].id);
this.selectedIds[i] = this.multipleSelection[i].id;
}
const _this = this;
axios({
method: "post",
url: "http://localhost:7777/brand/deleteByIds",
data: _this.selectedIds
}).then(function (resp) {
if (resp.data === "delete success") {
_this.selectAll();
_this.$message({
message: '删除成功',
type: 'success'
});
}
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
分页查询
我们之前做的 查询所有
功能中将数据库中所有的数据查询出来并展示到页面上,试想如果数据库中的数据有很多(假设有十几万条)的时候,将数据全部展示出来肯定不现实,那如何解决这个问题呢?几乎所有的网站都会使用分页解决这个问题。每次只展示一页的数据,比如一页展示10条数据,如果还想看其他的数据,可以通过点击页码进行查询
分析
分页查询sql
- 分页查询也是从数据库进行查询的,所以我们要分页对应的SQL语句应该怎么写。分页查询使用
LIMIT
关键字,格式为:LIMIT 开始索引 每页显示的条数
。以后前端页面在发送请求携带参数时,它并不明确开始索引是什么,但是它知道查询第几页。所以开始索引
需要在后端进行计算,计算的公式是 :开始索引 = (当前页码 - 1)* 每页显示条数 - 比如查询第一页的数据的 SQL 语句是:
select * from tb_brand limit 0,5;
- 查询第二页的数据的 SQL 语句是:
select * from tb_brand limit 5,5;
- 查询第三页的数据的 SQL 语句是:
select * from tb_brand limit 10,5;
前后端数据分析
分页查询功能时候比较复杂的,所以我们要先分析清楚以下两个问题:
- 前端需要传递什么参数给后端
根据上一步对分页查询 SQL 语句分析得出,前端需要给后端两个参数- 当前页码 :currentPage
- 每页显示条数:pageSize
- 后端需要响应什么数据给前端
- 当前页需要展示的数据。我们在后端一般会存储到 List 集合中
- 总共记录数。在上图页面中需要展示总的记录数,所以这部分数据也需要。总的页面 ElementUI 的分页组件会自动计算,我们不需要关心
- 将以上两部分封装到 PageBean 对象中,并将该对象转换为 json 格式的数据响应回给浏览器
- 通过上面的分析我们需要先在
pojo
包下创建PageBean
类,为了做到通用性会将其定义成泛型类,代码如下:
package pojo;
import java.util.List;
//分页查询的JavaBean
public class PageBean<T> {
// 总记录数
private int totalCount;
// 当前页数据
private List<T> rows;
public int getTotalCount() {
return totalCount;
}
public void setTotalCount(int totalCount) {
this.totalCount = totalCount;
}
public List<T> getRows() {
return rows;
}
public void setRows(List<T> rows) {
this.rows = rows;
}
}
后端实现
dao方法实现
- 在
BrandMapper
接口中定义selectByPage()
方法进行分页查询,代码如下:
/**
* 分页查询
* @param begin
* @param size
* @return
*/
@Select("select * from tb_brand limit #{begin} , #{size}")
@ResultMap("brandResultMap")
List<Brand> selectByPage(@Param("begin") int begin,@Param("size") int size);
- 在
BrandMapper
接口中定义selectTotalCount()
方法进行统计记录数,代码如下:
/**
* 查询总记录数
* @return
*/
@Select("select count(*) from tb_brand ")
int selectTotalCount();
service方法实现
在 BrandService
接口中定义 selectByPage()
分页查询数据的业务逻辑方法
/**
* 分页查询
* @param currentPage 当前页码
* @param pageSize 每页展示条数
* @return
*/
PageBean<Brand> selectByPage(int currentPage,int pageSize);
在 BrandServiceImpl
类中重写 selectByPage()
方法,并进行业务逻辑实现
@Override
public PageBean<Brand> selectByPage(int currentPage, int pageSize) {
//获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//获取BrandMapper
BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);
//计算索引
int begin = (currentPage - 1) * pageSize;
//计算当前条目数
//查询当前页数据
List<Brand> rows = mapper.selectByPage(begin, pageSize);
//查询总记录数
int totalCount = mapper.selectTotalCount();
//封装成PageBean对象
PageBean<Brand> brandPageBean = new PageBean<>();
brandPageBean.setRows(rows);
brandPageBean.setTotalCount(totalCount);
//释放资源
sqlSession.close();
return brandPageBean;
}
servlet方法实现
在BrandServlet类中定义selectByPage()
方法。而该方法的逻辑如下:
- 获取页面提交的
当前页码
和每页显示条目数
两个数据。这两个参数是在url后进行拼接的,格式是url?currentPage=1&pageSize=5
。获取这 样的参数需要使用requet.getparameter()
方法获取。 - 调用 service 的
selectByPage()
方法进行分页查询的业务逻辑处理 - 将查询到的数据转换为 json 格式的数据
- 响应 json 数据
- 获取页面提交的
servlet 中
selectByPage()
方法代码实现如下:
public void selectByPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 接收 当前页码 和 每页展示条数 url?currentPage=1&pageSize=5
String _currentPage = request.getParameter("currentPage");
String _pageSize = request.getParameter("pageSize");
int currentPage = Integer.parseInt(_currentPage);
int pageSize = Integer.parseInt(_pageSize);
//2. 调用service查询
PageBean<Brand> pageBean = brandService.selectByPage(currentPage, pageSize);
//2. 转为JSON
String jsonString = JSON.toJSONString(pageBean);
//3. 写数据
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(jsonString);
}
测试
在浏览器上地址栏输入 http://localhost:7777/brand/selectByPage?currentPage=1&pageSize=5
,可以查询到数据

前端实现
selectAll 代码改进
selectAll()
函数之前是查询所有数据,现需要改成分页查询。 请求路径应改为 http://localhost:7777/brand/selectByPage?currentPage=1&pageSize=5
,而 currentPage
和 pageSize
是需要携带的参数,分别是 当前页码
和 每页显示的条目数
。
刚才我们对后端代码进行测试可以看出响应回来的数据,所以在异步请求的成功回调函数(then
中的匿名函数)中给页面表格的数据模型赋值。整体代码如下
selectAll() {
var _this = this;
axios({
method:"post",
url:"http://localhost:7777/brand/selectByPage?currentPage="+this.currentPage+"&pageSize="+this.pageSize,
data:this.brand
}).then(function (resp) {
//设置表格数据
_this.tableData = resp.data.rows; // {rows:[],totalCount:100}
//设置总记录数
_this.totalCount = resp.data.totalCount;
})
}
改变当前页的条目数和当前页码
先来看看分页条的代码@size-change
就是每页显示的条目数发生变化时会触发的事件,而该事件绑定了一个 handleSizeChange
函数@current-change
就是页码发生变化时会触发的事件,而该事件绑定了一个 handleCurrentChange
函数
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[5, 10, 15, 20]"
:page-size="5"
layout="total, sizes, prev, pager, next, jumper"
:total="totalCount">
</el-pagination>
我们只需要在这两个函数中重新设置当前页的条目数和当前页码,然后调用selectAll
函数重新分页查询数据
handleSizeChange(val) {
//重新设置每页的条目数
this.pageSize=val;
this.selectAll();
},
handleCurrentChange(val) {
this.currentPage=val;
this.selectAll();
}
// 当前页
currentPage: 1,
// 每页显示多少数据
pageSize: 5,
// 总数据条数
totalCount : 0
条件查询
要做条件查询功能,先明确以下三个问题
- 3个条件之间什么关系?
- 同时满足,所用 SQL 中多个条件需要使用 and 关键字连接
- 3个条件必须全部填写吗?
- 不需要。想根据哪儿个条件查询就写那个,所以这里需要使用动态 sql 语句
- 条件查询需要分页吗?
- 需要
后端实现
dao实现
在 BrandMapper
接口中定义 selectByPageAndCondition()
方法 和 selectTotalCountByCondition
方法,用来进行条件分页查询功能,方法如下:
/**
* 分页条件查询
* @param begin
* @param size
* @return
*/
List<Brand> selectByPageAndCondition(@Param("begin") int begin,@Param("size") int size,@Param("brand") Brand brand);
/**
* 根据条件查询总记录数
* @return
*/
int selectTotalCountByCondition(Brand brand);
参数:
begin
分页查询的起始索引size
分页查询的每页条目数brand
用来封装条件的对象
由于这是一个复杂的查询语句,需要使用动态sql;所以我们在映射配置文件中书写 sql 语句。brand_name
字段和 company_name
字段需要进行模糊查询,所以需要使用 %
占位符。映射配置文件中 statement 书写如下:
<!--查询满足条件的数据并进行分页-->
<select id="selectByPageAndCondition" resultMap="brandResultMap">
select *
from tb_brand
<where>
<if test="brand.brandName != null and brand.brandName != '' ">
and brand_name like #{brand.brandName}
</if>
<if test="brand.companyName != null and brand.companyName != '' ">
and company_name like #{brand.companyName}
</if>
<if test="brand.status != null">
and status = #{brand.status}
</if>
</where>
limit #{begin} , #{size}
</select>
<!--查询满足条件的数据条目数-->
<select id="selectTotalCountByCondition" resultType="java.lang.Integer">
select count(*)
from tb_brand
<where>
<if test="brandName != null and brandName != '' ">
and brand_name like #{brandName}
</if>
<if test="companyName != null and companyName != '' ">
and company_name like #{companyName}
</if>
<if test="status != null">
and status = #{status}
</if>
</where>
</select>
service实现
在 BrandService
接口中定义 selectByPageAndCondition()
分页查询数据的业务逻辑方法
/**
* 分页条件查询
* @param currentPage
* @param pageSize
* @param brand
* @return
*/
PageBean<Brand> selectByPageAndCondition(int currentPage,int pageSize,Brand brand);
在 BrandServiceImpl
类中重写 selectByPageAndCondition()
方法,并进行业务逻辑实现
@Override
public PageBean<Brand> selectByPageAndCondition(int currentPage, int pageSize, Brand brand) {
//2. 获取SqlSession对象
SqlSession sqlSession = factory.openSession();
//3. 获取BrandMapper
BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);
//4. 计算开始索引
int begin = (currentPage - 1) * pageSize;
// 计算查询条目数
int size = pageSize;
// 处理brand条件,模糊表达式
String brandName = brand.getBrandName();
if (brandName != null && brandName.length() > 0) {
brand.setBrandName("%" + brandName + "%");
}
String companyName = brand.getCompanyName();
if (companyName != null && companyName.length() > 0) {
brand.setCompanyName("%" + companyName + "%");
}
//5. 查询当前页数据
List<Brand> rows = mapper.selectByPageAndCondition(begin, size, brand);
//6. 查询总记录数
int totalCount = mapper.selectTotalCountByCondition(brand);
//7. 封装PageBean对象
PageBean<Brand> pageBean = new PageBean<>();
pageBean.setRows(rows);
pageBean.setTotalCount(totalCount);
//8. 释放资源
sqlSession.close();
return pageBean;
}
注意:brandName 和 companyName 属性值到时候需要进行模糊查询,所以前后需要拼接上 %
servlet实现
在 BrandServlet
类中定义 selectByPageAndCondition()
方法。而该方法的逻辑如下:
获取页面提交的
当前页码
和每页显示条目数
两个数据。这两个参数是在url后进行拼接的,格式是url?currentPage=1&pageSize=5
。获取这样的参数需要使用requet.getparameter()
方法获取。获取页面提交的
条件数据
,并将数据封装到一个Brand对象中。由于这部分数据到时候是需要以 json 格式进行提交的,所以我们需要通过流获取数据,具体代码如下:// 获取查询条件对象 BufferedReader br = request.getReader(); String params = br.readLine();//json字符串 //转为 Brand Brand brand = JSON.parseObject(params, Brand.class);
调用 service 的
selectByPageAndCondition()
方法进行分页查询的业务逻辑处理将查询到的数据转换为 json 格式的数据
响应 json 数据
servlet 中 selectByPageAndCondition()
方法代码实现如下:
/**
* 分页条件查询
* @param request
* @param response
* @throws ServletException
* @throws IOException
*/
public void selectByPageAndCondition(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//处理乱码问题
request.setCharacterEncoding("utf-8");
//1. 接收 当前页码 和 每页展示条数 url?currentPage=1&pageSize=5
String _currentPage = request.getParameter("currentPage");
String _pageSize = request.getParameter("pageSize");
int currentPage = Integer.parseInt(_currentPage);
int pageSize = Integer.parseInt(_pageSize);
// 获取查询条件对象
BufferedReader br = request.getReader();
String params = br.readLine();//json字符串
//转为 Brand
Brand brand = JSON.parseObject(params, Brand.class);
//2. 调用service查询
PageBean<Brand> pageBean = brandService.selectByPageAndCondition(currentPage,pageSize,brand);
//2. 转为JSON
String jsonString = JSON.toJSONString(pageBean);
//3. 写数据
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(jsonString);
}
前端实现
前端代码我们从以下几方面实现:
查询表单绑定查询条件对象模型
这一步在页面上已经实现了,页面代码如下:<el-form :inline="true" :model="brand" class="demo-form-inline"> <el-form-item label="当前状态"> <el-select v-model="brand.status" placeholder="当前状态"> <el-option label="启用" value="1"></el-option> <el-option label="禁用" value="0"></el-option> </el-select> </el-form-item> <el-form-item label="企业名称"> <el-input v-model="brand.companyName" placeholder="企业名称"></el-input> </el-form-item> <el-form-item label="品牌名称"> <el-input v-model="brand.brandName" placeholder="品牌名称"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="onSubmit">查询</el-button> </el-form-item> </el-form>
点击查询按钮查询数据
从上面的代码可以看到给查询
按钮绑定了onSubmit()
函数,而在onSubmit()
函数中只需要调用selectAll()
函数进行条件分页查询。改进 selectAll() 函数
子页面加载完成后发送异步请求,需要携带当前页码、每页显示条数、查询条件对象。接下来先对携带的数据进行说明:当前页码
和每页显示条数
这两个参数我们会拼接到 URL 的后面查询条件对象
这个参数需要以 json 格式提交给后端程序- 修改
selectAll()
函数逻辑为
var _this = this; axios({ method:"post", url:"http://localhost:7777/brand/selectByPageAndCondition?currentPage="+this.currentPage+"&pageSize="+this.pageSize, data:this.brand }).then(function (resp) { //设置表格数据 _this.tableData = resp.data.rows; // {rows:[],totalCount:100} //设置总记录数 _this.totalCount = resp.data.totalCount; })
前端代码优化
咱们已经将所有的功能实现完毕。而针对前端代码中的发送异步请求的代码,如下
var _this = this;
axios({
method:"post",
url:"http://localhost:7777/brand/selectByPageAndCondition?currentPage="+this.currentPage+"&pageSize="+this.pageSize,
data:this.brand
}).then(function (resp) {
//设置表格数据
_this.tableData = resp.data.rows; // {rows:[],totalCount:100}
//设置总记录数
_this.totalCount = resp.data.totalCount;
})
需要在成功的回调函数(也就是then
函数中的匿名函数)中使用this,都需要在外边使用 _this
记录一下 this
所指向的对象;因为在外边的 this
表示的是 Vue 对象,而回调函数中的 this
表示的不是 vue 对象。这里我们可以使用 ECMAScript6
中的新语法(箭头函数)来简化这部分代码,如上面的代码可以简化为:
axios({
method:"post",
url:"http://localhost:7777/brand/selectByPageAndCondition?currentPage="+this.currentPage+"&pageSize="+this.pageSize,
data:this.brand
}).then((resp) => {
//设置表格数据
this.tableData = resp.data.rows; // {rows:[],totalCount:100}
//设置总记录数
this.totalCount = resp.data.totalCount;
})
箭头函数语法:
(参数) => {
逻辑代码
}
箭头函数的作用:
替换(简化)匿名函数。
补充
由于课程中没有删除和修改的功能,所以我这里完善了一下
删除功能
删除功能的难点在于如果获取本行数据的id,但成功获取id之后的操作就与批量删除的操作一样了
批量删除是选中多行数据,然后将多行数据的id放到ids数组中,那么单行数据就只需要将当前行的id放到ids数组中
具体操作就是this.selectedIds[0] = this.currentRow.id;
,前端代码也只需要将for循环替换掉
那现在我们只需要复制一份deleteByIds
,然后改个名,将for循环替换掉,就大功告成了
deleteById() {
this.$confirm('此操作将删除数据, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
//唯一的区别就是不用这个for循环来获取id了
/*for (let i = 0; i < this.multipleSelection.length; i++) {
this.selectedIds[i] = this.multipleSelection[i].id;
}
*/
this.selectedIds[0] = this.currentRow.id;
var _this = this;
axios({
method: "post",
url: "http://localhost:8080/test_brand/brand/deleteByIds",
data: _this.selectedIds
}).then(function (resp) {
if (resp.data == "success") {
_this.selectAll();
_this.$message({
message: '恭喜你,删除成功',
type: 'success'
});
}
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
}
如何获取当前行数据
其实我们在写前端页面的时候,就已经遇到过了,那现在我们仔细看看
Table 组件提供了单选的支持,只需要配置highlight-current-row属性即可实现单选。之后由current-change事件来管理选中时触发的事件,它会传入currentRow,oldCurrentRow。如果需要显示索引,可以增加一列el-table-column,设置type属性为index即可显示从 1 开始的索引号。
<template>
<el-table
ref="singleTable"
:data="tableData"
highlight-current-row
@current-change="handleCurrentChange"
style="width: 100%">
<el-table-column
type="index"
width="50">
</el-table-column>
<el-table-column
property="date"
label="日期"
width="120">
</el-table-column>
<el-table-column
property="name"
label="姓名"
width="120">
</el-table-column>
<el-table-column
property="address"
label="地址">
</el-table-column>
</el-table>
<div style="margin-top: 20px">
<el-button @click="setCurrent(tableData[1])">选中第二行</el-button>
<el-button @click="setCurrent()">取消选择</el-button>
</div>
</template>
<script>
export default {
data() {
return {
tableData: [{
date: '2016-05-02',
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, {
date: '2016-05-04',
name: '王小虎',
address: '上海市普陀区金沙江路 1517 弄'
}, {
date: '2016-05-01',
name: '王小虎',
address: '上海市普陀区金沙江路 1519 弄'
}, {
date: '2016-05-03',
name: '王小虎',
address: '上海市普陀区金沙江路 1516 弄'
}],
currentRow: null
}
},
methods: {
setCurrent(row) {
this.$refs.singleTable.setCurrentRow(row);
},
//这个方法直接搬走
handleCurrentChange(val) {
this.currentRow = val;
}
}
}
</script>
提取一下信息就是我们要在table中配置highlight-current-row
属性,current-change
事件,和事件绑定的handleCurrentChange
函数,然后在Vue的data中配置currentRow
属性(默认为null),然后就大功告成了
- table标签的修改如下
<el-table
:data="tableData"
style="width: 100%"
:row-class-name="tableRowClassName"
highlight-current-row
@current-change="handleCurrentChangeRow"
@selection-change="handleSelectionChange">
<!--我这里给绑定的事件改了个名-->
- data中新增属性(示例代码中已经提供好了)
currentRow: null
- methods中新增方法(示例代码中已经提供好了,直接CV,我这里改了个名)
handleCurrentChangeRow(val) {
this.currentRow = val;
}
修改功能
- 修改功能依旧是分为
回显
和修改
两部分 - 修改功能的需求就是当我们点击
修改
按钮(给修改按钮绑定echo()
回显函数)时,会弹出一个对话框,里面是已经填好了的品牌信息,我们只需要修改我们需要改的部分,其他部分不用动,然后点击提交按钮
(给提交按钮绑定update()
更新函数),完成修改功能
前端页面
- 前端页面我们直接照抄新增页面即可,注意将设置
:visible.sync="updateVisible"
,同时在Vue对象的data中,新增updateVisible属性updateVisible: false
,该属性默认为false,用来控制修改对话框是否显示
<!--对话框弹出修改品牌-->
<el-dialog
title="修改品牌"
:visible.sync="updateVisible"
width="30%">
<el-form ref="form" :model="brand" label-width="80px">
</el-form-item>
<el-form-item label="企业名称">
<el-input v-model="brand.companyName"></el-input>
</el-form-item>
<el-form-item label="品牌名称">
<el-input v-model="brand.brandName"></el-input>
</el-form-item>
<el-form-item label="排序">
<el-input v-model="brand.ordered"></el-input>
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="brand.status" active-value=1 inactive-value=0></el-switch>
</el-form-item>
<el-form-item label="备注">
<el-input type="textarea" v-model="brand.description"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="update">提交</el-button>
<el-button @click="updateVisible = false">取消</el-button>
</el-form-item>
</el-form>
</el-dialog>
回显功能
- 回显功能是要当我们点击
修改
按钮时,弹出对话框,同时显示原有数据,那我们就给修改按钮的点击事件绑定一个回显函数 - 而回显函数的功能就是刚刚说的,弹出对话框(设置
updateVisible为true
),显示原有数据(将本行数据赋给brand即可,因为修改对话框已经绑定了brand模型) - 代码如下
echo(){
this.updateVisible = true;
this.brand = this.currentRow;
}
修改功能
dao实现
- 接口方法声明如下
/**
* 更新数据
* @param brand
*/
@Update("update tb_brand set brand_name=#{brandName},company_name=#{companyName},ordered=#{ordered},`description`=#{description},`status`=#{status} where id =#{id}")
@ResultMap("brandResultMap")
void update(Brand brand);
service实现
- 在
BrandService
接口中定义update()
更新数据的业务逻辑方法
/**
* 更新数据
* @param brand
*/
void update(Brand brand);
- 在
BrandServiceImpl
类中重写update()
方法,并进行业务逻辑实现
@Override
public void update(Brand brand) {
SqlSession sqlSession = sqlSessionFactory.openSession();
BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);
mapper.update(brand);
sqlSession.commit();
sqlSession.close();
}
servlet实现
- servlet 中
update()
方法代码实现如下:
public void update(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//处理乱码问题
request.setCharacterEncoding("utf-8");
//1. 获取输入流,接收品牌数据json字符串
BufferedReader br = request.getReader();
String params = br.readLine();
//将json字符串转为brand对象
Brand brand = JSON.parseObject(params, Brand.class);
Integer id = brand.getId();
String brandName = brand.getBrandName();
String companyName = brand.getCompanyName();
String description = brand.getDescription();
Integer ordered = brand.getOrdered();
Integer status = brand.getStatus();
Brand b = new Brand();
b.setId(id);
b.setBrandName(brandName);
b.setCompanyName(companyName);
b.setDescription(description);
b.setOrdered(ordered);
b.setStatus(status);
//2. 调用service更新
brandService.update(brand);
//3. 响应成功的标识
response.getWriter().write("success");
}
前端实现
- 前面当我们点击
修改
按钮时,已经帮我们弹出对话框,且对话框中有原有的数据,当我们修改完毕之后,点击提交
按钮,就会帮我们完成更新操作
那现在我们只需要来写一个update()
函数就完事儿了,而update()函数和addBrand()函数几乎没有区别,只需要将url改成update,然后提示信息改一下,就大功告成了(区别就是底层的SQL语句不一样,addBrand()执行insert,update()执行update)
update() {
var _this = this;
axios({
method: "post",
url: "http://localhost:7777/brand/update",
data: _this.brand
}).then(function (resp) {
if (resp.data == "success") {
_this.updateVisible = false
_this.selectAll();
_this.$message({
message: '修改成功',
type: 'success'
});
}
})
}
最终代码

JavaWebFinal