前言:还记得我前面所写的博文《Java Web基础入门第二十九讲 基于Servlet+JSP+JavaBean开发模式的用户登录注册》吗?我们以前是创建代表数据库的XML文件来保存用户信息的,现在我们已经学习了数据库相关的知识,所以应把XML换成数据库,升级成数据库应用。
现在,我们把以前的工程复制并拷贝一份,假设以前的工程名是day09_user,复制一份并拷贝,重新修改工程名为day14_user,此刻将其直接部署在Tomcat服务器上,那么day14_user这个JavaWeb应用映射的虚拟目录仍然是/day09_user
,并不是映射成为一个同名的虚拟目录/day14_user
,这是一个经常被人忽略的问题,如要解决这个问题,可像下面这样做:
升级成数据库应用
首先,导入数据库驱动。
然后,为应用创建相应的库和表。
create database day14_user;
use day14_user;
create table users
(
id varchar(40) primary key,
username varchar(40) not null unique,
password varchar(40) not null,
email varchar(100) not null unique,
birthday date,
nickname varchar(40) not null
);
最后,在src目录下创建一个db.properties文件,如下图所示:
在db.properties中编写MySQL数据库的连接信息,代码如下所示:
重写UserDao
现在我们要重写UserDao接口中所有的方法,在cn.liayun.dao.impl包中创建一个UserDao接口的实现类——UserDaoJdbcImpl,如下所示:
由于一开始我们写的并不是最终的代码,所以我们将重点关注public User find(String username, String password)
登录方法,我们首先使用Statement对象重写该方法,代码如下:
@Override
public User find(String username, String password) {
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
st = conn.createStatement();
String sql = "select * from users where username='" + username + "' and password='" + password + "'";
rs = st.executeQuery(sql);
if (rs.next()) {
User user = new User();
user.setBirthday(rs.getDate("birthday"));
user.setEmail(rs.getString("email"));
user.setId(rs.getString("id"));
user.setNickname(rs.getString("nickname"));
user.setPassword(rs.getString("password"));
user.setUsername(rs.getString("username"));
return user;
}
return null;
} catch (Exception e) {
// 异常直接往上面抛,除了给上层带来麻烦,没有任何好处,所以可将异常转型然后往业务层抛
throw new RuntimeException(e);
} finally {
JdbcUtils.release(conn, st, rs);
}
}
在Java中关于对异常的处理真是太mb的烦了,下面我将总结一下在开发中对异常的处理原则。
Java开发中对异常的处理原则
在学界关于对Java异常的处理吵的热火朝天,主要分为三大门派:
- Java语言设计者——高司令,他设计出来的Java语言本具有编译时异常和运行时异常,既然Java语言都是他设计出来的,所以他认为编译时异常也是必须合理存在的;
- 《Thinking In Java》的作者认为Java语言的编译时异常就是垃圾,异常都应转为运行时异常抛出去;
- Spring框架的作者柔和了以上2人的观点,他就说——你这个有程序有异常,你拿到这个异常,怎么做呢?就看上一层程序能不能处理?如果不能处理,就转为运行时异常抛出去,如果能处理,就转为编译时异常直接往上抛出去。
所以,最好我们应采用Spring框架作者的观点,那么我们的处理方法就是:你这个有程序有异常,你拿到这个异常,怎么做呢?就看异常你希不希望上一层程序处理?如果你不希望上一层程序处理,免得给上一层程序带来麻烦,就转为运行时异常抛出去,如果你希望上一层程序处理,就转为编译时异常直接往上抛出去。
在实际开发中,最好每一层都编写一个自定义异常,例如在Dao层(数据访问层)自定义一个DaoException异常类。就拿这个用户登录注册的升级案例来说,应在cn.liayun.exception包中创建一个DaoException异常类,该类的代码如下:
package cn.liayun.exception;
public class DaoException extends RuntimeException {
public DaoException() {
// TODO Auto-generated constructor stub
}
public DaoException(String message) {
super(message);
// TODO Auto-generated constructor stub
}
public DaoException(Throwable cause) {
super(cause);
// TODO Auto-generated constructor stub
}
public DaoException(String message, Throwable cause) {
super(message, cause);
// TODO Auto-generated constructor stub
}
public DaoException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
// TODO Auto-generated constructor stub
}
}
我们要自定义一个Dao层异常类抛出去,这是为什么呢?自定义一个Dao层异常类抛出去最大的好处就是我把这个异常抛出去,人家收到这个异常,一看到这个异常的类名,就能知道到底是哪一层出问题了,他就可以快速定位到这一层来找问题,而且,最好每一层都要有一个自定义异常。于是,UserDaoJdbcImpl类中的find方法应修改为:
@Override
public User find(String username, String password) {
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
st = conn.createStatement();
String sql = "select * from users where username='" + username + "' and password='" + password + "'";
rs = st.executeQuery(sql);
if (rs.next()) {
User user = new User();
user.setBirthday(rs.getDate("birthday"));
user.setEmail(rs.getString("email"));
user.setId(rs.getString("id"));
user.setNickname(rs.getString("nickname"));
user.setPassword(rs.getString("password"));
user.setUsername(rs.getString("username"));
return user;
}
return null;
} catch (Exception e) {
// 异常直接往上面抛,除了给上层带来麻烦,没有任何好处,所以可将异常转型然后往业务层抛
/*
* 自定义一个Dao层异常类抛出去,为什么呢?
* 自定义一个Dao层异常类抛出去最大的好处就是我把这个异常抛出去,人家收到这个异常,一看到这个异常的类名,
* 就能知道到底是哪一层出问题了,他就可以快速定位到这一层来找问题。最好每一层都要有一个自定义异常。
*/
throw new DaoException(e);
} finally {
JdbcUtils.release(conn, st, rs);
}
}
实现Service层和Dao层的解耦
我们以前在开发Service层(Service层对Web层提供所有的业务服务)时,编写UserService接口的具体实现类——BusinessServiceImpl时,业务逻辑层和数据访问层是紧密联系在一起的,所以业务逻辑层和数据访问层要解耦(希望底层Dao层代码换了,业务逻辑层的代码一行都不改,这时就要用到工厂设计模式了),要解耦,有两种方法:
- 工厂设计模式
- Spring
现在,我们只重点关注工厂设计模式,因为后面大家终究是要学习Spring框架的。这时我们要定义一个Dao工厂,新建一个cn.liayun.factory包,在包中创建一个Dao工厂,即DaoFactory类。工厂一般要设计成单例的,这是为什么呢?工厂设计成单例的,工厂的对象在内存中只有一个,目的是希望所有的Dao都由一个工厂来生产。假设不把工厂设计成单例的,将来Dao由不同的工厂来生产,你觉得所有的Dao由一个工厂来生产更好,还是由不同的工厂来生产更好?答案显然是由一个工厂来生产更好,将来维护起来也好维护。
我们首先尝试着编写一下DaoFactory类的代码,也并不是一触而就就写得很好的。
public class DaoFactory {
private Properties daoConfig = new Properties();
private DaoFactory() {}
private static DaoFactory instance = new DaoFactory();
public static DaoFactory getInstance() {
return instance;
}
// 传入UserDao.class(接口类型)
// 传入DepartmentDao.class(接口类型)
public <T> T createDao(Class<T> clazz) {
// clazz.getName(); // 返回UserDao接口的完整名称,即cn.liayun.dao.UserDao
String name = clazz.getSimpleName(); // 返回UserDao接口的简单名称,即UserDao
DaoFactory.class.getClassLoader().getResourceAsStream("dao.properties");
......
}
}
以上代码写得好吗?显然这样写并不好,别人每次调用createDao方法,都会去读一下配置文件dao.properties,但此配置文件在整个系统里面只要读取一次就好,没必要老去读,即以下这行代码只需运行一次。
DaoFactory.class.getClassLoader().getResourceAsStream("dao.properties");
因此,这行代码可以放到静态代码块里。于是,以上代码是不是可以修改为:
public class DaoFactory {
static {
DaoFactory.class.getClassLoader().getResourceAsStream("dao.properties");
}
private Properties daoConfig = new Properties();
private DaoFactory() {}
private static DaoFactory instance = new DaoFactory();
public static DaoFactory getInstance() {
return instance;
}
// 传入UserDao.class(接口类型)
// 传入DepartmentDao.class(接口类型)
public <T> T createDao(Class<T> clazz) {
// clazz.getName(); // 返回UserDao接口的完整名称,即cn.liayun.dao.UserDao
String name = clazz.getSimpleName(); // 返回UserDao接口的简单名称,即UserDao
// DaoFactory.class.getClassLoader().getResourceAsStream("dao.properties");
......
}
}
虽然这样做并没有不好,但是对于现在而言还有一种更优的做法,DaoFactory这个类被设计成单例的,所以其构造函数仅执行一次,因此可将读取dao.properties配置文件的那行代码放到DaoFactory这个类的构造函数里面。所以最后完整的代码应如下:
package cn.liayun.factory;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class DaoFactory {
private Properties daoConfig = new Properties();
private DaoFactory() {
InputStream in = DaoFactory.class.getClassLoader().getResourceAsStream("dao.properties");
try {
daoConfig.load(in);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static DaoFactory instance = new DaoFactory();
public static DaoFactory getInstance() {
return instance;
}
// 传入UserDao.class
// 传入DepartmentDao.class
public <T> T createDao(Class<T> clazz) {
// clazz.getName(); // 返回UserDao接口的完整名称,即cn.liayun.dao.UserDao
String name = clazz.getSimpleName();// 返回UserDao接口的简单名称,即UserDao
String className = daoConfig.getProperty(name);
try {
T dao = (T) Class.forName(className).newInstance();// 创建实例对象
return dao;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
紧接着,在src目录下创建一个dao.properties文件,该配置文件的内容如下:
UserDao=cn.liayun.dao.impl.UserDaoJdbcImpl
最后,实现Service层和Dao层的解耦,UserService接口的具体实现类——BusinessServiceImpl只须修改为:
package cn.liayun.service.impl;
import cn.liayun.dao.UserDao;
import cn.liayun.domain.User;
import cn.liayun.exception.UserExistException;
import cn.liayun.factory.DaoFactory;
import cn.liayun.utils.ServiceUtils;
//对Web层提供所有的业务服务
public class BusinessServiceImpl {
/*
* 业务逻辑层和数据访问层需要解耦。
* 要解耦,有两种方法:工厂设计模式 or Spring
*/
// private UserDao dao = new UserDaoJdbcImpl();
private UserDao dao = DaoFactory.getInstance().createDao(UserDao.class);//工厂设计模式
//对Web层提供注册服务
public void register(User user) throws UserExistException {
//先判断当前要注册的用户是否存在
boolean b = dao.find(user.getUsername());
if (b) {
/*
* Service层是由Web层来调用的,
* 发现当前要注册的用户已存在,要提醒给Web层,Web层给用户一个友好提示。
* 希望Web层一定要处理,处理之后给用户一个友好提示,所以抛一个编译时异常,
* 抛运行时异常是不行的,因为Web层可处理可不处理。
*/
throw new UserExistException();//发现要注册的用户已存在,则给Web层抛一个编译时异常,提醒Web层处理这个异常,给用户一个友好提示。
} else {
user.setPassword(ServiceUtils.md5(user.getPassword()));
dao.add(user);
}
}
//对Web层提供登录服务
public User login(String username, String password) {
password = ServiceUtils.md5(password);// 要把密码md5一把再找
return dao.find(username, password);
}
}
防范SQL注入攻击
SQL注入是用户利用某些系统没有对输入数据进行充分的检查,从而进行恶意破坏的行为。例如,Statement存在SQL注入攻击问题,假如登录用户名采用' or 1=1 or username='
。
对于防范SQL注入,可以采用PreparedStatement取代Statement。
PreparedStatement对象介绍
PreperedStatement是Statement的孩子,它的实例对象可以通过调用Connection.preparedStatement()方法获得,相对于Statement对象而言:
- PreperedStatement可以避免SQL注入的问题;
- Statement会使数据库频繁编译SQL,可能造成数据库缓冲区溢出。PreparedStatement可对SQL进行预编译,从而提高数据库的执行效率;
- 并且PreperedStatement对于SQL中的参数,允许使用占位符的形式进行替换,简化SQL语句的编写。
使用PreparedStatement改写UserDaoJdbcImpl类中的find(String username, String password)方法。
@Override
public User find(String username, String password) {
Connection conn = null;
PreparedStatement st = null;//PreparedStatement预防SQL注入
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
String sql = "select * from users where username=? and password=?";
st = conn.prepareStatement(sql);//预编译这条sql语句
st.setString(1, username);//st.setString(1, "' or 1=1 or username='");
st.setString(2, password);
rs = st.executeQuery();
if (rs.next()) {
User user = new User();
user.setBirthday(rs.getDate("birthday"));
user.setEmail(rs.getString("email"));
user.setId(rs.getString("id"));
user.setNickname(rs.getString("nickname"));
user.setPassword(rs.getString("password"));
user.setUsername(rs.getString("username"));
return user;
}
return null;
} catch (Exception e) {
throw new DaoException(e);
} finally {
JdbcUtils.release(conn, st, rs);
}
}
到此为止,我们就可以完整地写出UserDaoJdbcImpl类了,其完整代码如下:
package cn.liayun.dao.impl;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import cn.liayun.dao.UserDao;
import cn.liayun.domain.User;
import cn.liayun.exception.DaoException;
import cn.liayun.utils.JdbcUtils;
public class UserDaoJdbcImpl implements UserDao {
@Override
public void add(User user) {
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
String sql = "insert into users(id,username,password,email,birthday,nickname) values(?,?,?,?,?,?)";
st = conn.prepareStatement(sql);
st.setString(1, user.getId());
st.setString(2, user.getUsername());
st.setString(3, user.getPassword());
st.setString(4, user.getEmail());
st.setDate(5, new java.sql.Date(user.getBirthday().getTime()));
st.setString(6, user.getNickname());
int num = st.executeUpdate();
if (num < 1) {
throw new RuntimeException("注册用户失败!");
}
} catch (Exception e) {
throw new DaoException(e);
} finally {
JdbcUtils.release(conn, st, rs);
}
}
@Override
public User find(String username, String password) {
Connection conn = null;
PreparedStatement st = null;//PreparedStatement预防SQL注入
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
String sql = "select * from users where username=? and password=?";
st = conn.prepareStatement(sql);//预编译这条sql语句
st.setString(1, username);//st.setString(1, "' or 1=1 or username='");
st.setString(2, password);
rs = st.executeQuery();
if (rs.next()) {
User user = new User();
user.setBirthday(rs.getDate("birthday"));
user.setEmail(rs.getString("email"));
user.setId(rs.getString("id"));
user.setNickname(rs.getString("nickname"));
user.setPassword(rs.getString("password"));
user.setUsername(rs.getString("username"));
return user;
}
return null;
} catch (Exception e) {
throw new DaoException(e);
} finally {
JdbcUtils.release(conn, st, rs);
}
}
@Override
public boolean find(String username) {
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
String sql = "select * from users where username=?";
st = conn.prepareStatement(sql);
st.setString(1, username);
rs = st.executeQuery();
if (rs.next()) {
return true;
}
return false;
} catch (Exception e) {
throw new DaoException(e);
} finally {
JdbcUtils.release(conn, st, rs);
}
}
}
开发完数据访问层,一定要对程序已编写好的部分代码进行测试,做一步,测试一步,以免整个程序完成后由于页面太多或者是代码量太大给查找错误造成更大的负担!所以,在juint.test包下创建了一个UserDaoJdbcTest类,该类的具体代码如下:
package junit.test;
import java.util.Date;
import org.junit.Test;
import cn.liayun.dao.UserDao;
import cn.liayun.dao.impl.UserDaoJdbcImpl;
import cn.liayun.domain.User;
public class UserDaoJdbcTest {
@Test
public void testAdd() {
User user = new User();
user.setBirthday(new Date());
user.setEmail("haha@qq.com");
user.setId("101");
user.setNickname("草拟");
user.setPassword("321");
user.setUsername("haha");
UserDao dao = new UserDaoJdbcImpl();
dao.add(user);
}
@Test
public void testFind() {
UserDao dao = new UserDaoJdbcImpl();
User user = dao.find("haha", "321");// 在断点模式Watch
System.out.println(user);
}
@Test
public void testFindByUsername() {
UserDao dao = new UserDaoJdbcImpl();
System.out.println(dao.find("haha"));
}
}
经测试,没发现任何错误。同样也要对业务逻辑层已编写好的部分代码进行测试,在juint.test包下创建了一个ServiceTest类,该类的具体代码如下:
package junit.test;
import java.util.Date;
import org.junit.Test;
import cn.liayun.domain.User;
import cn.liayun.exception.UserExistException;
import cn.liayun.service.impl.BusinessServiceImpl;
public class ServiceTest {
@Test
public void testRegister() {
User user = new User();
user.setBirthday(new Date());
user.setEmail("yeer@qq.com");
user.setId("102");
user.setNickname("叶二");
user.setPassword("321");
user.setUsername("yeer");
BusinessServiceImpl service = new BusinessServiceImpl();
try {
service.register(user);
System.out.println("注册成功!!!");
} catch (UserExistException e) {
System.out.println("用户已存在");
}
}
@Test
public void testLogin() {
BusinessServiceImpl service = new BusinessServiceImpl();
User user = service.login("yeer", "321");
System.out.println(user);
}
}
经测试,没发现任何错误。到此,对基于Servlet+JSP+JavaBean开发模式的用户登录注册的升级改造算是圆满完成了。
面试题:Statement和PreparedStatement的区别
Statement和PreparedStatement的区别有以下3点:
- PreparedStatement是Statement的孩子;
- PreparedStatement可以防止SQL注入的问题;
- PreparedStatement会对SQL语句进行预编译,以减轻数据库服务器的压力。就像xxx.java→xxx.class→JVM执行一样,SQL语句→编译→数据库执行。