基于Servlet的MVC框架Dome(二):数据库初步

282 阅读4分钟

简介

在前面进行了相关的初始化后,能访问相关的hello页面,本次编写初步的数据库操作部分

工程GitHub地址:github.com/lw124392545…

需求场景

用户访问注册页面,填写相关的信息后,注册到后台数据库中

代码编写

依赖相关

本地需要使用内存数据H2,添加相关的依赖到模块:my-web-mvc

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.194</version>
        </dependency>

建立相关的JSP页面

JSP相当于前端页面,建立下面三个:

  • register.jsp :用户注册页面
  • login.jsp :用户登录页面,用作注册成功后的展示,还没有具体逻辑
  • failed.jsp :失败页面

对应内容如下:

<%@ page import="java.util.Enumeration" %>
<head>
    <jsp:directive.include file="/WEB-INF/jsp/prelude/include-head-meta.jspf" />
    <title>注册</title>
</head>
<body>
<div class="container">
    <h1>用户注册</h1>
    <form action="/register" method="post">
        <table>
            <tr>
                <td>用户:<input name="user" type="text"></td>
            </tr>
            <tr>
                <td>密码:<input name="password" type="text"></td>
            </tr>
            <tr>
                <td><input name="submit" type="submit" value="提交"></td>
            </tr>
        </table>
    </form>
</div>
</body>
<%@ page import="java.util.Enumeration" %>
<head>
    <jsp:directive.include file="/WEB-INF/jsp/prelude/include-head-meta.jspf" />
    <title>登录</title>
</head>
<body>
<div class="container">
    <h1>用户登录</h1>
    <h6>已注册成功</h6>
    <form action="/register" method="post">
        <table>
            <tr>
                <td>用户:<input name="user" type="text"></td>
            </tr>
            <tr>
                <td>密码:<input name="password" type="text"></td>
            </tr>
            <tr>
                <td><input name="submit" type="submit" value="提交"></td>
            </tr>
        </table>
    </form>
</div>
</body>
<head>
    <jsp:directive.include
            file="/WEB-INF/jsp/prelude/include-head-meta.jspf" />
    <title>My Home Page</title>
</head>
<body>
<div class="container-lg">
    <!-- Content here -->
    操作失败
</div>
</body>

建立完后,重启应用是可以通过连接进行访问的,如:localhost:8080/register.jsp

初始化数据库源

简单直接的使用下面的方式加载数据库驱动,并建立后相关的用户表,代码大致如下:

import java.sql.*;
import java.util.Properties;

public class H2Database {

    public static H2Database getInstance() {
        return new H2Database();
    }

    private Connection connection;

    private H2Database() {
        try {
            init();
        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        }
    }

    private static final String CREATE_USERS_TABLE_DDL_SQL = "create table mvc_user ( " +
            "id integer auto_increment," +
            "name varchar(20)," +
            "password varchar(20)," +
            "email varchar(20)," +
            "phoneNumber varchar(20)," +
            "primary key (`id`));";

    private void init() throws ClassNotFoundException, SQLException {
        Class.forName("org.h2.Driver");
        Driver driver = DriverManager.getDriver("jdbc:h2:mem:myDb;DB_CLOSE_DELAY=-1");
        connection = driver.connect("jdbc:h2:mem:myDb;DB_CLOSE_DELAY=-1", new Properties());
        Statement statement = connection.createStatement();
        // 创建 users 表
        System.out.println(statement.execute(CREATE_USERS_TABLE_DDL_SQL));
        statement.close();
    }

    public Connection getConnection() {
        return connection;
    }
}

Controllers编写

新建一个 RegisterController,从请求中读取用户和密码(其他属性后面再完善)

成功跳转到登录页面,失败跳转到失败页面

import org.geektimes.projects.user.domain.User;
import org.geektimes.projects.user.service.UserService;
import org.geektimes.projects.user.service.impl.UserServiceImpl;
import org.geektimes.web.mvc.controller.PageController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.POST;
import javax.ws.rs.Path;

@Path("/register")
public class RegisterController implements PageController {

    private UserService userService = new UserServiceImpl();

    @Override
    @POST
    public String execute(HttpServletRequest request, HttpServletResponse response) throws Throwable {
        String user = request.getParameter("user");
        String password = request.getParameter("password");
        System.out.printf("user: %s, password: %s\n", user, password);
        if (user == null || password == null) {
            return "register.jsp";
        }

        if (userService.register(new User(user, password, "1", "1"))) {
            return "login.jsp";
        }
        return "failed.jsp";
    }
}

Service编写

这么模仿Mybatis,Repository使用代码生成,先调用register方法,其他的后面完善

import org.geektimes.projects.user.domain.User;
import org.geektimes.projects.user.repository.UserRepository;
import org.geektimes.projects.user.service.UserService;
import org.geektimes.web.mvc.dbProxy.JdkRepositoryProxy;

public class UserServiceImpl implements UserService {

    UserRepository userRepository = JdkRepositoryProxy.create(UserRepository.class);

    @Override
    public boolean register(User user) {
        return userRepository.save(user);
    }

    @Override
    public boolean deregister(User user) {
        return false;
    }

    @Override
    public boolean update(User user) {
        return false;
    }

    @Override
    public User queryUserById(Long id) {
        return null;
    }

    @Override
    public User queryUserByNameAndPassword(String name, String password) {
        return null;
    }
}

Repository编写

这里直接在接口中使用自己写的注解:Insert、Select

  • Insert :除了列名和值,其余的交给程序自动生成
  • Select :查询语句目前完全由用户填写,同时传入需要返回的类型

用过Mybatis的应该能懂大致意思,注解和Repository如下:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Insert {

    String value();
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Select {

    String value();

    String returnType();
}
import org.geektimes.web.mvc.sql.Insert;
import org.geektimes.web.mvc.sql.Select;
import org.geektimes.projects.user.domain.User;

import java.util.Collection;

/**
 * 用户存储仓库
 *
 * @since 1.0
 */
public interface UserRepository  {

    @Insert("insert into mvc_user (%s) values (%s)")
    boolean save(User user);

    boolean deleteById(Long userId);

    boolean update(User user);

    User getById(Long userId);

    User getByNameAndPassword(String userName, String password);

    @Select(value = "select * from mvc_user;", returnType = "org.geektimes.projects.user.domain.User")
    Collection<User> getAll();
}

Repository代理编写

这里代码需要缓存起来,毕竟是没有状态的,使用一个实例就够了,第一次获取需要生成,后面直接后台缓存里面的

import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class JdkRepositoryProxy {

    private static Map<String, Object> repositoryMap = new ConcurrentHashMap<>();

    public static <T> T create(Class clazz) {
        // 查询是否之前生成过,存储的直接返回
        if (!repositoryMap.containsKey(clazz.getName())) {
            repositoryMap.put(clazz.getName(), newProxy(clazz));
        }
        return (T) repositoryMap.get(clazz.getName());
    }

    private static <T> T newProxy(Class clazz) {
        ClassLoader loader = JdkRepositoryProxy.class.getClassLoader();
        Class[] classes = new Class[]{clazz};
        return (T) Proxy.newProxyInstance(loader, classes, new RepositoryInvocationHandler());
    }
}

在具体的代理处理中,目前先写了两个简单的插入和查询处理

  • 插入处理:假定传入的对象属性和列对应,直接拼接生成相关语句即可
  • 查询语句:更加注册传入的需要返回的类路径,可以生成相关的类实例,然后使用反射进行相关的设置
import org.geektimes.web.mvc.sql.Insert;
import org.geektimes.web.mvc.sql.Select;
import org.geektimes.web.mvc.database.H2Database;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author lw1243925457
 */
public class RepositoryInvocationHandler implements InvocationHandler {

    H2Database database = H2Database.getInstance();

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        Annotation[] annotations = method.getDeclaredAnnotations();
        for (Annotation annotation: annotations) {
            if (annotation instanceof Insert) {
                try {
                    return executeInsertSql(((Insert) annotation).value(), args);
                } catch (IllegalAccessException | SQLException e) {
                    e.printStackTrace();
                    return false;
                }
            }

            if (annotation instanceof Select) {
                try {
                    return executeQuerySql(((Select) annotation).value(), ((Select) annotation).returnType());
                } catch (SQLException | ClassNotFoundException | IllegalAccessException | IntrospectionException | NoSuchMethodException | InvocationTargetException | InstantiationException e) {
                    e.printStackTrace();
                }
            }
        }
        return true;
    }

    private Object executeQuerySql(String querySql, String returnType) throws SQLException, ClassNotFoundException, IllegalAccessException, IntrospectionException, NoSuchMethodException, InvocationTargetException, InstantiationException {
        List<Object> result = new ArrayList<>();

        Connection conn = database.getConnection();
        Statement statement = conn.createStatement();

        ResultSet res = statement.executeQuery(querySql);
        // BeanInfo
        BeanInfo userBeanInfo = Introspector.getBeanInfo(Class.forName(returnType), Object.class);

        while (res.next()) {
            Object object = Class.forName(returnType).newInstance();
            for (PropertyDescriptor propertyDescriptor : userBeanInfo.getPropertyDescriptors()) {
                String fieldName = propertyDescriptor.getName();
                Class fieldType = propertyDescriptor.getPropertyType();
                String methodName = typeMethodMappings.get(fieldType);
                // 可能存在映射关系(不过此处是相等的)
                String columnLabel = fieldName;
                Method resultSetMethod = ResultSet.class.getMethod(methodName, String.class);
                // 通过放射调用 getXXX(String) 方法
                Object resultValue = resultSetMethod.invoke(res, columnLabel);
                // 获取 User 类 Setter方法
                // PropertyDescriptor ReadMethod 等于 Getter 方法
                // PropertyDescriptor WriteMethod 等于 Setter 方法
                Method setterMethodFromUser = propertyDescriptor.getWriteMethod();
                // 以 id 为例,  user.setId(resultSet.getLong("id"));
                setterMethodFromUser.invoke(object, resultValue);
            }

            System.out.println(object.toString());
        }

        statement.close();
        return result;
    }

    /**
     * 数据类型与 ResultSet 方法名映射
     */
    static Map<Class, String> typeMethodMappings = new HashMap<>();

    static {
        typeMethodMappings.put(Long.class, "getLong");
        typeMethodMappings.put(String.class, "getString");
    }

    private Object executeInsertSql(String sqlTemplate, Object[] args) throws IllegalAccessException, SQLException {
        System.out.println("execute insert: " + sqlTemplate);
        StringBuilder cols = new StringBuilder();
        StringBuilder values = new StringBuilder();

        Object data = args[0];
        Field[] fields = data.getClass().getDeclaredFields();
        for (Field field: fields) {
            if (field.get(data) == null) {
                continue;
            }
            cols.append(field.getName()).append(",");
            values.append(field.get(data)).append(",");
        }

        String sql = String.format(sqlTemplate, cols.toString(), values.toString());
        System.out.println("sql string: " + sql);

        Connection conn = database.getConnection();
        Statement statement = conn.createStatement();
        statement.execute(sql);

        statement.close();
        return true;
    }
}

运行测试

# 进入user-platform目录
cd user-platform
mvn clean package -U
java -jar .\user-web\target\user-web-v1-SNAPSHOT-war-exec.jar

时间有点紧,目前注册只能使用数字,不支持英文

按照上面的命令运行相应的程序后,访问用户注册链接:http://localhost:8080/register

成功后成功跳转到用户登录页面