首页 > 程序开发 > 软件开发 > Java >

对基于Servlet+JSP+JavaBean开发模式的用户登录注册的升级

2016-08-23

还记得我前面所写的博文基于Servlet+JSP+JavaBean开发模式的用户登录注册吗?我们以前是创建代表数据库的xml文件来保存用户信息的,现在我们已经学习了数据库相关的知识,所以应把xml换成数据库,升级成数据库应

我们以前是创建代表数据库的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数据库的连接信息,代码如下所示:

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


#driver=oracle.jdbc.driver.OracleDriver


#url=jdbc:oracle:thin:@localhost:1521:orcl


#username=system


#password=111

重写UserDao

现在我们要重写UserDao接口中所有的方法,在cn.itcast.dao.impl包中创建一个UserDao接口的实现类——UserDaoJdbcImpl,如下所示:
这里写图片描述
由于一开始我们写的并不是最终的代码,所以我们将重点关注public User find(String username, String password)登录方法。
我们首先使用statement对象重写public User find(String username, String password)方法,代码如下:

@Override
public User find(String username, String password) {
    Connection conn = null;
    Statement st = null; // PreparedStatement预防SQL注入
    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语言都是他设计出来的,所以他认为编译时异常也是必须合理存在的。 《Thinking In Java》的作者认为java语言编译时异常就是垃圾,异常都应转为运行时异常抛出去。 Spring的作者柔和了以上2人的观点,他就说——你这个有程序有异常,你拿到这个异常,怎么做呢?就看上一层程序能不能处理?如果不能处理,就转为运行时异常抛出去,如果能处理,就转为编译时异常直接往上抛出去。

所以,最好我们应采用Spring的作者的观点,那我们的处理方法就是:你这个有程序有异常,你拿到这个异常,怎么做呢?就看异常你希不希望上一层程序处理?如果你不希望上一层程序处理,免得给上一层程序带来麻烦,就转为运行时异常抛出去,如果你希望上一层程序处理,就转为编译时异常直接往上抛出去
在实际开发中,最好每一个层都编写一个自定义异常,例如在Dao层(数据访问层)自定义一个异常类——DaoException。
在cn.itcast.exception包中创建异常类DaoException,如下:
这里写图片描述
DaoException类代码如下:

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类中的public User find(String username, String password)方法为:

@Override
public User find(String username, String password) {
    Connection conn = null;
    Statement st = null; // PreparedStatement预防SQL注入
    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接口的具体实现类——UserServiceImpl时,业务逻辑层和数据访问层是紧密联系在一起的,所以业务逻辑层和数据访问层要解耦(希望底层Dao层代码换了,业务逻辑层的代码一行都不改,这时要用到工厂设计模式),要解耦,有两种方法:

工厂模式 Spring

现在我们重点关注工厂模式。这时我们要定义一个Dao工厂,新建一个cn.itcast.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 createDao(Class clazz) {

        // clazz.getName(); // 返回UserDao接口的完整名称:cn.itcast.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 createDao(Class clazz) {

        // clazz.getName(); // 返回UserDao接口的完整名称:cn.itcast.dao.UserDao
        String name = clazz.getSimpleName(); // 返回UserDao接口的简单名称:UserDao

        // DaoFactory.class.getClassLoader().getResourceAsStream("dao.properties");
        ......
    }
}

以上这样做并没有不好,但是对于现在而言还有一种做法,DaoFactory这个类被设计成单例的,所以其构造函数仅执行一次,所以可放到DaoFactory这个类的构造函数里面。所以最后完整的代码如下:

public class DaoFactory {
    /*
    static {
        DaoFactory.class.getClassLoader().getResourceAsStream("dao.properties");
    }
    */
    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 createDao(Class clazz) {

        // clazz.getName(); // 返回UserDao接口的完整名称:cn.itcast.dao.UserDao
        String name = clazz.getSimpleName(); // 返回UserDao接口的简单名称:UserDao

        // DaoFactory.class.getClassLoader().getResourceAsStream("dao.properties");

        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文件,如下图所示:
这里写图片描述
在db.properties中内容如下所示:

UserDao=cn.itcast.dao.impl.UserDaoJdbcImpl

实现service层和dao层的解耦,UserService接口的具体实现类——UserServiceImpl只须修改为:

// 对web层提供所有的业务服务
public class BusinessServiceImpl {
    /*
     * 业务逻辑层和数据访问层要解耦——希望底层Dao层代码换了,业务逻辑层的代码一行都不改,这时要用到工厂设计模式
     * 要解耦,有两张方法:
     * 1. 工厂模式
     * 2. 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) { // aaa 123
        password = ServiceUtils.md5(password); // 要把密码md5一把再找
        return dao.find(username, password);
    }
}

防范sql注入攻击

SQL注入是用户利用某些系统没有对输入数据进行充分的检查,从而进行恶意破坏的行为。
例如,statement存在sql注入攻击问题,假如登录用户名采用' or 1=1 or name='。
这里写图片描述

对于防范SQL注入,可以采用PreparedStatement取代Statement。

PreparedStatement对象介绍

PreperedStatement是Statement的孩子,它的实例对象可以通过调用Connection.preparedStatement()方法获得,相对于Statement对象而言:

PreperedStatement可以避免SQL注入的问题。 Statement会使数据库频繁编译SQL,可能造成数据库缓冲区溢出。PreparedStatement可对SQL进行预编译,从而提高数据库的执行效率。 并且PreperedStatement对于sql中的参数,允许使用占位符的形式进行替换,简化sql语句的编写。

使用PreparedStatement改写UserDaoJdbcImpl类的public User 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(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类了,其完整代码如下:

public class UserDaoJdbcImpl implements UserDao {

    @Override
    public void add(User user) {
        Connection conn = null;
        // Statement st = 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());

            // st = conn.createStatement();
            // String sql = "insert into users(id,username,password,email,birthday,nickname) values('"+user.getId()+"','"+user.getUsername()+"','"+user.getPassword()+"','"+user.getEmail()+"','"+user.getBirthday().toLocaleString()+"','"+user.getNickname()+"')";
            // int num = st.executeUpdate(sql);

            int num = st.executeUpdate();
            if(num < 1) {
                throw new RuntimeException("注册用户失败!!!");
            }
        } catch (Exception e) {
            // 异常直接往上面抛,除了给上层带来麻烦,没有任何好处,所以可将异常转型然后往业务层抛
            /*
             * 自定义一个Dao异常抛出去,为什么呢?
             * 自定义一个Dao异常抛出去最大的好处就是我把这个异常抛出去,人家收到这个异常,一看到这个异常的类名,
             * 就能知道到底是哪一层出问题了,他就可以快速定位到这一层来找问题。最好每一层都要有一个自定义异常。
             */

            /*
             * gosling    Thinking In Java   Spring
             * Spring作者——你这个有程序有异常,你拿到这个异常,怎么做呢?就看上一层程序能不能处理?
             *            如果不能处理,就转为运行时异常抛出去,如果能处理,就转为编译时异常直接往上抛出去。
             *            
             * 我们的处理方法——你这个有程序有异常,你拿到这个异常,怎么做呢?就看异常你希不希望上一层程序处理?
             *            如果你不希望上一层程序处理,免得给上一层程序带来麻烦,就转为运行时异常抛出去,如果你希望上一层程序处理,就转为编译时异常直接往上抛出去。
             */
            // throw new RuntimeException(e);
            throw new DaoException(e);
        } finally {
            JdbcUtils.release(conn, st, rs);
        }
    }

    // username=&#39;or 1=1 or username=&#39;   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(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);
        }
    }

}

面试题:Statement和PreparedStatement的区别。
答:

PreparedStatement是Statement的孩子。 PreparedStatement可以防止sql注入的问题。 PreparedStatement会对sql语句进行预编译,以减轻数据库服务器的压力。

就像xxx.java&rarr;xxx.class&rarr;JVM执行一样,sql语句&rarr;编译&rarr;数据库执行。
开发完数据访问层,一定要对程序已编写好的部分代码进行测试,做一步,测试一步,以免整个程序完成后由于页面太多或者是代码量太大给查找错误造成更大的负担!所以,在juint.test包下创建了一个UserDaoJdbcTest类。
这里写图片描述
UserDaoJdbcTest类的具体代码如下:

public class UserDaoJdbcTest {
    @Test
    public void testAdd() {
        User user = new User();
        user.setBirthday(new Date());
        user.setEmail("bb@sina.com");
        user.setId("2142354354");
        user.setNickname("李子");
        user.setUsername("bbbb");
        user.setPassword("123");

        UserDao dao = new UserDaoJdbcImpl();
        dao.add(user);
    }

    @Test
    public void testFind() {
        UserDao dao = new UserDaoJdbcImpl();
        User user = dao.find("bbbb", "123"); // 在断点模式Watch
        System.out.println(user);
    }

    @Test
    public void testFindByUsername() {
        UserDao dao = new UserDaoJdbcImpl();
        System.out.println(dao.find("bbbb"));
    }
}

经测试,没发现任何错误。同样也要对业务逻辑层已编写好的部分代码进行测试,在juint.test包下创建了一个ServiceTest类。
这里写图片描述
ServiceTest类的具体代码如下:

public class ServiceTest {

    @Test
    public void testRegister() {
        User user = new User();
        user.setBirthday(new Date());
        user.setEmail("bb@sina.com");
        user.setId("2142354354");
        user.setNickname("李子");
        user.setUsername("lizi");
        user.setPassword("123");

        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("lizi", "123");
        System.out.println(user);
    }
}

经测试,没发现任何错误。
到此,对基于Servlet+JSP+JavaBean开发模式的用户登录注册的升级改造圆满完成。

相关文章
最新文章
热点推荐