系统设计
一个web项目,先从原始需求开始分析,找出需求中涉及到的Use Case(案例),然后涉及表结构,画原型图,定义URL规范。
1.设计用例
找出功能点,可以用一张UML的”用例图“来描绘以上用例,这样效果会更好,UML流程图可以用visio画图。
2.设计表结构
根据需求,找到核心的业务实体,创建对应的表。
建议:
表明与字段名均为小写,若多个单词可用“下划线”分割;
每张表都要有一个唯一ID主键字段
数据类型尽可能统一,不要出现太多数据类型
个人一般比较喜欢用powerdesigner,设计好表后可以直接生成sql语句。
3.设计界面原型
可以使用Balsqmiq Mockups软件,这是一款比较Q的软件,可以快速地画出界面原型,感受一下画风。
(这个画风让我想起了饥荒这款游戏。。。。。)
4.设计URL
设计出url与页面跳转及接口的对应关系
举个栗子
URL | 描述 |
GET:/customer | 进入列表查询界面 |
GET:/customer_create | 进入新增界面 |
POST:/customer_create | 新增 |
DELETE:/customer_delete/{id} | 删除 |
创建数据库
创建数据库编码方式统一为UTF-8,以免编码不一致导致中文乱码。
建立连接后,New Database输入Database Name和Character set,点击确定。
这里使用的工具为Navicat。
准备开发环境
新建一个maven项目,具体步骤参照上一篇笔记。
这里不用框架,用servlet+jsp写一个mvc架构的项目。
编写模型层
数据库中添加表
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注解即可
编写服务层
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<com.smart4j.chapter2.model.Customer> * @Author: qiuyu * @Date: 2018/8/13 */ public List<Customer> 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<String,Object> fieldMap){ //todo return false; } /** * @Description: 更新客户 * @Param: [] * @return: boolean * @Author: qiuyu * @Date: 2018/8/13 */ public boolean updateCustomer(long id,Map<String,Object> fieldMap){ //todo return false; } /** * @Description: 删除客户 * @Param: [] * @return: boolean * @Author: qiuyu * @Date: 2018/8/13 */ public boolean deleteCustomer(long id){ //todo return false; } }
编写单元测试
首先要在pom.xml中添加依赖
<!--JUnit--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </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<Customer> 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<String,Object> fieldMap = new HashMap<String, Object>(); 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<String,Object> fieldMap = new HashMap<String,Object>(); 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进行转发或者重定向。
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>客户管理-创建客户</title> </head> <body> <h1>创建客户界面</h1> <%--TODO--%> </body> </html>
日志
在pom.xml中添加日志依赖
<!--slf4j--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.77</version> </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"); } }
数据库
添加mysql依赖以及两个apache常用依赖
<!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.33</version> <scope>runtime</scope> </dependency> <!--Apache Commons Lang--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.3.2</version> </dependency> <!--Apache Commons Collections--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.0</version> </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中插入依赖
<!-- Apache commons DbUtils --> <dependency> <groupId>commons-dbutils</groupId> <artifactId>commons-dbutils</artifactId> <version>1.6</version> </dependency>
然后在DBHelper类中增添以下方法。
private static final QueryRunner QUERY_RUNNER = new QueryRunner(); /** * 查询实体列表 * @param entityClass * @param sql * @param params * @param <T> * @return */ public static <T> List<T> queryEntityList(Class<T> entityClass,String sql,Object... params){ List<T> entityList; Connection conn = getConnection(); try { entityList = QUERY_RUNNER.query(conn,sql,new BeanListHandler<T>(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 <T> * @return */ public static <T> T queryEntity(Class<T> entityClass,String sql,Object... params){ T entity; Connection conn = getConnection(); try { entity=QUERY_RUNNER.query(conn,sql,new BeanHandler<T>(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<Map<String,Object>> executeQuery(String sql,Object... params){ List<Map<String,Object>> 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 <T> * @return */ public static <T> boolean insertEntity(Class<T> entityClass,Map<String,Object> 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 <T> * @return */ public static <T> boolean updateEntity(Class<T> entityClass,long id,Map<String,Object> 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<Object> paramList = new ArrayList<Object>(); paramList.addAll(fieldMap.values()); paramList.add(id); Object[] params = paramList.toArray(); return executeUpdate(sql,params)==1; } /** * 删除 * @param entityClass * @param id * @param <T> * @return */ public static <T> boolean deleteEntity(Class<T> 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中加入
<!--数据库连接池Apache DBCP--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-dbcp2</artifactId> <version>2.0.1</version> </dependency>
DBHelper.java,用连接池获取conn,把所有的关闭连接池方法注释掉。
/*数据库连接池*/ private static final ThreadLocal<Connection> 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<Connection>(); 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', '[email protected]', NULL); INSERT INTO `customer`(`name`, `contact`, `telephone`, `email`, `remark`) VALUES ('customer2', 'Rose', '13654565445', '[email protected]', 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加载类或读取文件。
在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<Customer> 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<Customer> 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架构变得更加轻量级。如何开发架构,看这里