架构探险笔记2

系统设计

一个web项目,先从原始需求开始分析,找出需求中涉及到的Use Case(案例),然后涉及表结构,画原型图,定义URL规范。

1.设计用例

找出功能点,可以用一张UML的”用例图“来描绘以上用例,这样效果会更好,UML流程图可以用visio画图。

2.设计表结构

根据需求,找到核心的业务实体,创建对应的表。

建议:

表明与字段名均为小写,若多个单词可用“下划线”分割;

每张表都要有一个唯一ID主键字段

数据类型尽可能统一,不要出现太多数据类型

个人一般比较喜欢用powerdesigner,设计好表后可以直接生成sql语句。

3.设计界面原型

可以使用Balsqmiq Mockups软件,这是一款比较Q的软件,可以快速地画出界面原型,感受一下画风。

《架构探险笔记2》

(这个画风让我想起了饥荒这款游戏。。。。。)

 4.设计URL

设计出url与页面跳转及接口的对应关系

举个栗子

URL  描述
GET:/customer 进入列表查询界面
GET:/customer_create 进入新增界面
POST:/customer_create 新增
DELETE:/customer_delete/{id}  删除

创建数据库

创建数据库编码方式统一为UTF-8,以免编码不一致导致中文乱码。

建立连接后,New Database输入Database Name和Character set,点击确定。

《架构探险笔记2》《架构探险笔记2》

这里使用的工具为Navicat。

准备开发环境

新建一个maven项目,具体步骤参照上一篇笔记。 

这里不用框架,用servlet+jsp写一个mvc架构的项目。

编写模型层

 《架构探险笔记2》

数据库中添加表

CREATE TABLE `customer`(
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(255) DEFAULT NULL,
    `contact` VARCHAR(255) DEFAULT NULL,
    `telephone` VARCHAR(255) DEFAULT NULL,
    `email` VARCHAR(255) DEFAULT NULL,
    `remark` text,
     PRIMARY KEY(`id`)
)ENGINE=INNODB DEFAULT CHARSET=utf8;

编写控制器层

一个servlet只有一个请求路径,但可以处理多种不同的请求,请求类型有GET、POST、PUT、DELETE

这里因为是servlet3.0所以不用在web.xml中配置servlet及映射mapping,只需要用@WebServlet注解即可

《架构探险笔记2》

编写服务层

package com.smart4j.chapter2.service;/**
 * Created by Administrator on 2018/8/13.
 */

import com.smart4j.chapter2.model.Customer;

import java.util.List;
import java.util.Map;

/**
 * @program: chapter2->CustomerService
 * @description: 客户服务层
 * @author: qiuyu
 * @create: 2018-08-13 20:15
 **/
public class CustomerService {
    
    /** 
    * @Description: 获取客户列表
    * @Param: [] 
    * @return: java.util.List 
    * @Author: qiuyu
    * @Date: 2018/8/13 
    */ 
    public List getCustomerList(String keyWord){
        //TODO
        return  null;
    }

    /** 
    * @Description: 根据id获取客户
    * @Param: [id] 
    * @return: com.smart4j.chapter2.model.Customer 
    * @Author: qiuyu
    * @Date: 2018/8/13 
    */ 
    public Customer getCustomer(long id){
        //todo
        return null;
    }
    /**
    * @Description:  创建客户
    * @Param: [fieldMap]
    * @return: boolean
    * @Author: qiuyu
    * @Date: 2018/8/13
    */
    public boolean createCustomer(Map fieldMap){
        //todo
        return false;
    }

    /** 
    * @Description: 更新客户
    * @Param: [] 
    * @return: boolean 
    * @Author: qiuyu
    * @Date: 2018/8/13 
    */ 
    public boolean updateCustomer(long id,Map fieldMap){
        //todo
        return false;
    }

    /** 
    * @Description: 删除客户
    * @Param: [] 
    * @return: boolean 
    * @Author: qiuyu
    * @Date: 2018/8/13 
    */ 
    public boolean deleteCustomer(long id){
        //todo
        return false;
    }
    
}

编写单元测试

首先要在pom.xml中添加依赖

        
        <dependency>
            <groupId>junitgroupId>
            <artifactId>junitartifactId>
            <version>4.11version>
            <scope>testscope>
        dependency>

编写单元测试

/**
 * Created by Administrator on 2018/8/13.
 */

import com.smart4j.chapter2.model.Customer;
import com.smart4j.chapter2.service.CustomerService;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @program: chapter2->CustomerServiceText
 * @description: 客户测试类
 * @author: qiuyu
 * @create: 2018-08-13 20:24
 **/
public class CustomerServiceText {
    private final CustomerService customerService;

    public CustomerServiceText() {
        customerService = new CustomerService();
    }

    @Before
   public void init(){
        //初始化数据库
   }

   @Test
   public void getCustomerListText(){
       List customerList =customerService.getCustomerList("");
       Assert.assertEquals(2,customerList.size());
   }
   @Test
   public void getCustomerTest(){
       long id =1;
       Customer customer = customerService.getCustomer(id);
       Assert.assertNotNull(customer);
   }
   @Test
   public void createCustomerTest(){
       Map fieldMap = new HashMap();
       fieldMap.put("name","小猪佩琪");
       fieldMap.put("contact","John");
       fieldMap.put("telephone","18151449650");
       boolean result = customerService.createCustomer(fieldMap);
       Assert.assertTrue(result);
   }

   @Test
   public void updateCustomerTest(){
       long id =1;
       Map fieldMap = new HashMap();
       fieldMap.put("contact","Aeolian");
       boolean result = customerService.updateCustomer(id,fieldMap);
       Assert.assertTrue(result);
   }

   @Test
   public void deleteCustomer(){
       long id =1;
       boolean result = customerService.deleteCustomer(id);
       Assert.assertTrue(result);
   }
}

视图层

使用JSP充当视图层,在WEB-INF/view目录下存放所有的JSP文件。推荐将JSP放入到WEB-INF内部,因为用户无法通过浏览器地址栏直接请求放在WEB-INF内部的JSP文件,必须通过Servlet进行转发或者重定向。

《架构探险笔记2》

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>客户管理-创建客户title>
head>
<body>

<h1>创建客户界面h1>
<%--TODO--%>
body>
html>

日志 

在pom.xml中添加日志依赖

        
        <dependency>
            <groupId>org.slf4jgroupId>
            <artifactId>slf4j-log4j12artifactId>
            <version>1.77version>
        dependency>

在main/resource目录下新建一个名为log4j.properties的文件

log4j.rootLogger=ERROR,console,file

log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%m%n

log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.File=${user.home}/logs/book.log
log4j.appender.file.DatePattern='_'yyyyMMdd
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{HH:mm:ss,SSS} %p %c (%L) -%m %n

log4j.logger.org.smart4j=DEBUG

将日志级别设置为DEBUG,并提供了两种日志appender,分别是console与file。最后一句制定只有org.smart4j包下的类才能输出DEBUG级别的日志。

测试,这里user.home为C:\Users\Administrator

public class PropsUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(PropsUtil.class);

    public static void main(String[] args) throws IOException {
        /*测试日志*/
        LOGGER.error("text");
    }
}

《架构探险笔记2》

数据库

添加mysql依赖以及两个apache常用依赖

        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>5.1.33version>
            <scope>runtimescope>
        dependency>
        
        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-lang3artifactId>
            <version>3.3.2version>
        dependency>
        
        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-collections4artifactId>
            <version>4.0version>
        dependency>

配置confg.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://******:3306/demo2?characterEncoding=utf-8
jdbc.username=root
jdbc.password=***密码****
jdbc.wait_timeout=600

编写常用工具类properties文件读写、类型转换、字符串工具类、集合工具类

详细链接Java开发常用Util工具类-StringUtil、CastUtil、CollectionUtil、PropsUtil

数据库操作类

数据库操作类DBHelper.java,用于初始化数据库,打开连接,关闭连接。 

package com.smart4j.chapter.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/**
 * @program: DBHelper
 * @description: 数据库操作类
 **/
public class DBHelper {
    private static final Logger LOGGER = LoggerFactory.getLogger(DBHelper.class);

    private static final String DRIVER;
    private static final String URL;
    private static final String USERNAME;
    private static final String PASSWORD;

    /**
     * 静态代码块在类加载时运行
     */
    static{
        Properties conf = PropsUtil.loadProps("config.properties");
        DRIVER = conf.getProperty("jdbc.driver");
        URL = conf.getProperty("jdbc.url");
        USERNAME = conf.getProperty("jdbc.username");
        PASSWORD = conf.getProperty("jdbc.password");

        try {
            Class.forName(DRIVER);
        } catch (ClassNotFoundException e) {
            //e.printStackTrace();
            LOGGER.error("can not load jdbc driver",e);
        }
    }

    /**
     * 获取数据库连接
     * @return
     */
    public static Connection getConnection(){
        Connection conn = null;
        try {
            conn = DriverManager.getConnection(URL,USERNAME,PASSWORD);
        } catch (SQLException e) {
            e.printStackTrace();  //在catlina.out中打印
            LOGGER.error("get connection failure",e);
        }
        return conn;
    }

    /**
     * 关闭数据库连接
     * @param conn
     */
    public static void closeConnection(Connection conn){
        if (conn!=null){
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
                LOGGER.error("close connection failure",e);
            }
        }
    }
}

 使用Apache Common项目中的DbUtils类库,pom.xml中插入依赖

        
        <dependency>
            <groupId>commons-dbutilsgroupId>
            <artifactId>commons-dbutilsartifactId>
            <version>1.6version>
        dependency>

然后在DBHelper类中增添以下方法。

    private static final QueryRunner QUERY_RUNNER = new QueryRunner();

    /**
     * 查询实体列表
     * @param entityClass
     * @param sql
     * @param params
     * @param 
     * @return
     */
    public static  List queryEntityList(Class entityClass,String sql,Object... params){
        List entityList;
        Connection conn = getConnection();
        try {
            entityList = QUERY_RUNNER.query(conn,sql,new BeanListHandler(entityClass),params);
        } catch (SQLException e) {
            //e.printStackTrace();
            LOGGER.error("query entity list failure",e);
            throw new RuntimeException(e);
        } finally {
            closeConnection(conn);
        }
        return entityList;
    }

    /**
     * 查询实体
     * @param entityClass
     * @param sql
     * @param params
     * @param 
     * @return
     */
    public static  T queryEntity(Class entityClass,String sql,Object... params){
        T entity;
        Connection conn = getConnection();
        try {
            entity=QUERY_RUNNER.query(conn,sql,new BeanHandler(entityClass),params);
        } catch (SQLException e) {
            //e.printStackTrace();
            LOGGER.error("query entity failure",e);
            throw new RuntimeException(e);
        } finally {
            closeConnection(conn);
        }
        return entity;
    }

    /**
     * 多表查询,其中的Map表示列明与列值的映射关系
     * @param sql
     * @param params
     * @return
     */
    public static List> executeQuery(String sql,Object... params){
        List> result;
        Connection conn = getConnection();
        try {
            result = QUERY_RUNNER.query(conn,sql,new MapListHandler(),params);
        } catch (SQLException e) {
            //e.printStackTrace();
            LOGGER.error("execute query failure",e);
            throw new RuntimeException(e);
        }
        return result;
    }

    /**
     * 执行更新语句(包括update,insert,delete)
     * @param sql
     * @param params
     * @return
     */
    public static int executeUpdate(String sql,Object... params){
        int rows =0;
        Connection conn = getConnection();
        try {
            rows = QUERY_RUNNER.update(conn,sql,params);
        } catch (SQLException e) {
            //e.printStackTrace();
            LOGGER.error("execute update failure",e);
            throw new RuntimeException(e);
        } finally {
            closeConnection(conn);
        }
        return rows;
    }

    /**
     * 插入实体(根据executeUpdate方法)
     * @param entityClass
     * @param fieldMap
     * @param 
     * @return
     */
    public static  boolean insertEntity(Class entityClass,Map fieldMap){
        if (CollectionUtil.isEmpty(fieldMap)){
            LOGGER.error("can not insert entity: fieldMap is empty");
            return false;
        }

        String sql = "insert into "+getTableName(entityClass);
        StringBuilder columns = new StringBuilder("(");
        StringBuilder values = new StringBuilder("(");
        for (String fieldName:fieldMap.keySet()){
            columns.append(fieldName).append(", ");
            values.append("?, ");
        }
        columns.replace(columns.lastIndexOf(", "),columns.length(),")");
        values.replace(values.lastIndexOf(", "),values.length(),")");
        sql += columns+" VALUES" +values;
        Object[] params = fieldMap.values().toArray();
        return executeUpdate(sql,params)==1;
    }

    /**
     * 修改
     * @param entityClass
     * @param id
     * @param fieldMap
     * @param 
     * @return
     */
    public static  boolean updateEntity(Class entityClass,long id,Map fieldMap){
        if (CollectionUtil.isEmpty(fieldMap)){
            LOGGER.error("can not update entity: fieldMap is empty");
            return false;
        }

        String sql = "update "+getTableName(entityClass) + " set ";
        StringBuilder columns = new StringBuilder();
        for (String fieldName:fieldMap.keySet()){
            columns.append(fieldName).append("=?, ");
        }
        sql+= columns.substring(0,columns.lastIndexOf(", "))+"where id=?";

        List paramList = new ArrayList();
        paramList.addAll(fieldMap.values());
        paramList.add(id);
        Object[] params = paramList.toArray();
        return executeUpdate(sql,params)==1;
    }

    /**
     * 删除
     * @param entityClass
     * @param id
     * @param 
     * @return
     */
    public static  boolean deleteEntity(Class entityClass,long id){
        String sql = "delete from "+getTableName(entityClass)+" where id =?";
        return executeUpdate(sql,id)==1;
    }

    /**
     * 获取类名(不包含报名和后缀)
     * @param entityClass
     * @return
     */
    private static String getTableName(Class entityClass){
        return entityClass.getSimpleName();
    }

数据库连接池 

为了不频繁的创建和关闭连接浪费资源,我们使用数据库连接池Apache DBCP。

pom.xml中加入

        
        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-dbcp2artifactId>
            <version>2.0.1version>
        dependency>

DBHelper.java,用连接池获取conn,把所有的关闭连接池方法注释掉。

    /*数据库连接池*/
    private static final ThreadLocal CONNECTION_HOLDER;
    private static final BasicDataSource DATA_SOURCE;

    /**
     * 静态代码块在类加载时运行
     */
    static{
        Properties conf = PropsUtil.loadProps("config.properties");
        DRIVER = conf.getProperty("jdbc.driver");
        URL = conf.getProperty("jdbc.url");
        USERNAME = conf.getProperty("jdbc.username");
        PASSWORD = conf.getProperty("jdbc.password");

        //数据库连接池
        CONNECTION_HOLDER = new ThreadLocal();
        DATA_SOURCE = new BasicDataSource();
        DATA_SOURCE.setDriverClassName(DRIVER);
        DATA_SOURCE.setUrl(URL);
        DATA_SOURCE.setUsername(USERNAME);
        DATA_SOURCE.setPassword(PASSWORD);
        /*使用连接池就不需要jdbc加载驱动了*/
        /*try {
            Class.forName(DRIVER);
        } catch (ClassNotFoundException e) {
            //e.printStackTrace();
            LOGGER.error("can not load jdbc driver",e);
        }*/
    }

    /**
     * 获取数据库连接
     * @return
     */
    public static Connection getConnection(){
        Connection conn = null;
        conn = CONNECTION_HOLDER.get();

        if (conn==null){  //当从连接池中获取的connection为null时新建一个Connection
            try {
                /*JDBC获取连接*/
                //conn = DriverManager.getConnection(URL,USERNAME,PASSWORD);
                /*数据库连接池获取连接*/
                    conn = DATA_SOURCE.getConnection();
            } catch (SQLException e) {
                e.printStackTrace();  //在catlina.out中打印
                LOGGER.error("get connection failure",e);
            } finally {
                /*数据库连接池,新建的情况要把新建的connection放入到池中*/
                CONNECTION_HOLDER.set(conn);
            }
        }
        return conn;
    }

测试 

连接池写好,下面我们测试一下,这里存在一个问题,就是执行deleteCustomerTest方法就不能在执行getCustomerTest方法了。因为得到的结果受到了影响。

JUnit在调用每个@Test方法前,都会调用@Before方法,也就是我们在单元测试类里定义的init,先在这个方法里加个TODO。需要在这里准备一个测试的数据库。

为了使开发数据库与测试数据库分离,也就是说,应该是两个数据库,只是表结构相同而已。我们需要为单元测试创建一个数据库,命名为demo_test,需要将demo_test数据库的customer表复制到demo_test数据库中。

新建测试数据库具体操作:

1.进入demo数据库,选中customer表,使用Ctrl+C复制表。

2.进入到demo_test数据库,使用Ctrl+V粘贴表。

3.右击customer表,单机Truncate Table清空表数据

准备一个customer_init.sql文件,用于存放所有的insert语句,改文件放在test/resources/sql下面,具体内容如下

TRUNCATE customer;
INSERT INTO `customer`(`name`, `contact`, `telephone`, `email`, `remark`) VALUES ('customer1', 'Jack', '18151449650', 'jack@gmail.com', NULL);
INSERT INTO `customer`(`name`, `contact`, `telephone`, `email`, `remark`) VALUES ('customer2', 'Rose', '13654565445', 'rose@gmail.com', NULL);

先Truncate清空表数据,然后再执行insert语句,不需要提供id,因为id是自增的。

技巧:默认情况下,IDEA不会在test下创建resources目录,可用alt+insert快捷键创建resources目录。右键该目录,单击Mark Directory As/Resources Root,该目录设为Maven的测试资源目录。需要注意的是,main/java、main/resources、test/java、test/resources四个目录都是classpath的根目录,当运行单元测试时,遵循“就近原则”,即有先从test/java、test/resources加载类或读取文件

《架构探险笔记2》

在test/resources目录下提供一个config.properties文件,这样执行单元测试的时候会优先在test/resources中找配置文件,若没有,再去main/resources中找。

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://*****:3306/****_test?characterEncoding=utf-8
jdbc.username=root
jdbc.password=******
jdbc.wait_timeout=600

 DBHelper.java中加入执行sql文件的方法

    /**
     * 逐行执行sql文件
     * @param filePath
     */
    public static void executeSqlFile(String filePath){
        //获取sql文件的InputStream流
        InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(filePath);
        //将InputStream流转为Reader
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));

        try {
            String sql;
            while ((sql = reader.readLine())!=null){  //逐行读取
                executeUpdate(sql);  //执行读取出来的一行sql语句
            }
        } catch (IOException e) {
            //e.printStackTrace();
            LOGGER.error("execute sql file failure",e);
            throw new RuntimeException(e);
        }
    }

在@before的方法中运行sql文件确保每次执行的测试数据库中的数据都是初始的。

    @Before
   public void init() throws IOException {
        DBHelper.executeSqlFile("sql/customer_init.sql");
   }

测试技巧:把光标放在方法外部,运行Run(Shift+F10)或Debug(Shift+F9),可测试所有方法。如果在某个测试方法内,智能执行光标所在的方法。

完善Controller层

/**
 * @description: 进入客户列表界面
 **/
@WebServlet("/customer")
public class CustomerServlet extends HttpServlet{
    private CustomerService service;

    @Override
    public void init() throws ServletException {
        super.init();
        service = new CustomerService();
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        List customerList = service.getCustomerList("");
        req.setAttribute("customerList",customerList);
        req.getRequestDispatcher("/WEB-INF/view/customer.jsp").forward(req,resp);
    }
}

在CustomerServlet中定义一个CustomerService变量,并在init方法中进行初始化。这样可以避免创建多个service实例,其实整个web应用只需要一个CustomerService实例。后续可以使用”单例模式”进行优化

在doGet方法中,我们调用了CustomerService对象的getCustomerList方法来获取List对象,并将其放入请求属性中,最后通过请求对象重定向到customer.jsp视图。

完善视图层

<%@ page pageEncoding="utf-8" contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core"%>

<c:set var="BASE" value="${pageContext.request.contextPath}">c:set>
<html>
<head>
    <title>客户管理title>
head>
<body>

<h1>客户列表h1>
<table>
    <tr>
        <th>客户名称th>
        <th>联系人th>
        <th>电话号码th>
        <th>邮箱地址th>
        <th>操作th>
    tr>
    <c:forEach var="customer" items="${customerList}">
        <tr>
            <td>${customer.name}td>
            <td>${customer.contact}td>
            <td>${customer.telephone}td>
            <td>${customer.email}td>
            <td>
                <a href="${BASE}/customer_edit?id=${customer.id}">编辑a>
                <a href="${BASE}/customer_delete?id=${customer.id}">删除a>
            td>
        tr>
    c:forEach>
table>
body>
html>

技巧:IDEA中查找文件的三种方法:1.Ctrl+N根据java类名查找文件;2.Ctrl+Shift+N,根据任意文件名进行查找;3.在Project目录上,Ctrl+Shift+F在指定路径下进行查找。

 

每一个请求都要对应一个Servlet,随着需求增多,Servlet类会越来越多。如何把多个业务模块的Servlet合并到一个类里面,例如:

@Controller
public class CustomerController {

    private CustomerService customerService;

    /**
     * 进入客户端界面
     */
    @Action("get:/customer")
    public View index(){
        List customerList = customerService.getCustomerList();
        return new View("customer.jsp").addModel("customerList",customerList);
    }

    /**
     * 显示客户基本信息
     */
    @Action("get:/customer_show")
    public View show(Param param){
        long id = param.getLong(id);
        Customer customer = customerService.getCustomer(id);
        return new View("customer_show.jsp").addModel("customer",customer);
    }

    /**
     * 删除客户信息
     * @param param
     * @return
     */
    @Action("delete:/customer_edit")
    public Data delete(Param param){
        long id = param.getLong(id);
        boolean result = customerService.deleteCustomer(id);
        return new Data(result);
    }
}

1.在类控制上,使用Controller注解,表明该类是一个控制器。

2.在成员变量上,使用Inject注解,自动创建该成员变量的实例

3.在控制类中包含若干方法(称为Action),每个方法都会使用Get/Post/Put/Delete注解,用于指定”请求类型“与”请求路径“。

4.在Action的参数中,通过Param对象封装所有请求参数,可根据getLong、getFieldMap等方法

5.在Action的返回值中,使用View封装一个JSP页面,使用Data封装一个Json数据。

有了这一个Controller,Servlet数量瞬间降下来。这种方式将MVC架构变得更加轻量级。如何开发架构,看这里

 

点赞

Leave a Reply

Your email address will not be published. Required fields are marked *