Java项目_一本糊涂账_Debug开发日志

本篇记录了Java项目 ”一本糊涂账“ 的Debug开发日志。包括项目总结,整体架构复盘。

整体架构

”一本糊涂账“是一个Java + MySQL项目,对数据库中的数据进行简单统计和显示的项目。
主体架构分为:(1)数据库MySQL设计 (2)UI设计 (3)JDBC传递MySQL数据库与Java程序的接口
三层架构之间的关系为:UI中的数据与数据库MySQL中的数据通过JDBC进行交互

我们要解决的需求:
(1)需要记录每一笔消费,这笔消费要对应不同分类当天日期相应备注
(2)分类信息可以自行添加增减修改
(3)对消费记录信息进行简单统计,并用图标形式进行更改
(4)能够设置当月经费预算,并能够对数据库进行备份和恢复

针对以上主体架构和需求分析,我们需要构建的包如下:

  • gui.frame => 对应UI设计主窗格
  • gui.panel => 对应UI设计
    • gui.service => 对应UI中相应的服务,在这里调用JDBC
    • gui.listener => 对应于UI中相应的控件所触发的服务
    • gui.model => UI中相关表格和相关数据统计可视化
  • entity => 对应MySQL中相应的表结构,对应MySQL数据库中每一条数据在Java程序中的实例
  • dao => JDBC实现,调用sql语句进行相应的数据库操作,并将数据库数据对应到entity实例
  • util => 小功能实现,比如java.sql.Date与java.Util.Date之间的转化
  • startup => 总程序入口,建立一个线程入口

项目总结复盘

数据库MySQL设计

如何构建数据库表结构是应该最先考虑的事情,也就是如何设置原始数据。后续的一切功能都需要建立在这个数据库上。

  • 针对需求1:记录表格。记录每一笔消费,每一条记录具有属性 消费金额消费类别消费日期备注 这四个属性。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    create table record(
    id int auto_increment, #主键
    spend int, # 消费金额
    cid int, #消费类别
    comment varchar(255), #备注
    date Date, #消费日期
    primary key(id),
    constraint fk_record_category foreign key (cid) references category(id)
    )ENGINE=InnoDB DEFAULT CHARSET=utf8;
    其中我们注意到消费类别是有限固定的,因此需要建立另一个表对应消费类别。这个消费类别对应另一个表中的主键数字。
  • 针对消费类别:记录消费类别。
    1
    2
    3
    4
    5
    create table category(
    id int auto_increment, #主键
    name varchar(255), #分类的名称
    primary key(id)
    )ENGINE=InnoDB DEFAULT CHARSET=utf8;
  • 针对需求4:需要进行相关设置。建立一个表来配置相关设置
    1
    2
    3
    4
    5
    6
    create table config(
    id int auto_increment, #主键
    key_ varchar(255), #设置名称 key_
    value varchar(255), #设置内容 value
    primary key (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  • 相应的对应于Java中的entity数据实例如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class Category { //对应消费类别
    public int id;
    public String name;
    public int recordNumber; //这个其实没有用到
    public method(){}
    }
    public class Config { //对应数据库设置
    public int id;
    public String key;
    public String value;
    public method(){}
    }
    public class Record { //对应每条消费记录
    public int id;
    public int spend;
    public int cid;//对应category中的类别id
    public String comment;
    public Date date;
    public method(){}
    }
    相关sql建立
  • 建立数据库并使用该数据库
    1
    2
    3
    drop database if exists hutubill; #检查是否存在该数据库
    create database hutubill; #如果不存在则创建该数据库
    use hutubill; #定位到该数据库
  • 定义表属性
    1
    2
    3
    CREATE TABLE table_name (column_name column_type);
    example:
    `runoob_title` VARCHAR(100) NOT NULL #通过设置NOT NULL, 当输入数据位NULL的时候,mysql会报错
  • 设置主键
    1
    2
    primary key (column_name) #设置主键
    auto_increment #设置自增加,一般设置在主键上
  • 设置存储引擎
    1
    2
    ENGINE 设置存储引擎, InnoDB是数据存储过后,关闭数据库数据仍然存在。
    CHARSET 设置编码,utf-8编码包含了中文

    JDBC使用

    JDBC是Mysql提供给Java语言的一个接口。主要目的是通过Java程序来操作Mysql数据库。

获取Mysql连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package util;

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

/**
* 建立DB连接
* @author shaoguoliang
*
*/
public class DBUtil {
static String ip = "127.0.0.1";
static int port = 3306; //问题是如何确定是mysql是在3306端口,在mysql Workbench查看
static String database = "hutubill";
static String encoding = "UTF-8";
static String loginName = "root";
static String password = "shaoguoliang";
static{
try {
Class.forName("com.mysql.cj.jdbc.Driver"); //注意与你的Mysql版本相对应
} catch (ClassNotFoundException e) {
// TODO: handle exception
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException{
String url = String.format("jdbc:mysql://%s:%d/%s?characterEncoding=%s", ip, port, database, encoding);
return DriverManager.getConnection(url,loginName,password);
}
}

JDBC的基本使用模式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
String sql = "insert into record values (NULL, ?, ?, ?, ?)";
try(Connection connection = new DBUtil.getConnection();
PreparedStatement ps = connection.preparedStatement(sql, Statement.RETURN_GENERATED_KEYS);){ //这里需要带上Statement.RETURN_GENERATED_KEYS才能使sql返回的结果成功转化为ResultSet
ps.setInt(1, record.spend); //设置相应数据表的数据,注意要与数据表中的设置一一对应
ps.setInt(2, record.cid);
ps.setString(3, record.comment);
ps.setDate(4, DateUtil.util2sql(record.date));
ps.execute();
ResultSet rs = ps.getGeneratedKeys(); //返回执行结果
while(rs.next()){ //判断返回结果rs是否为空
record.id = rs.getInt(1); //这里数据表中的第一位为id主键
}
} catch (Exception e){
e.printStackTrace();
}

使用经验:

  1. 尽量使用PreparedStatement,这样你可以轻而易举的控制能够加载sql语句中的那几个限定的类型,而不是自己将他转化成String的形式 https://www.runoob.com/mysql/mysql-data-types.html
  2. 在使用日期查询的过程中,sql语句中的date格式必须是yyyy-mm-dd HH:MM:SS. 比如在进行查询两个日期中间的数据时:
    String sql = "select * from record where date >= ? and date <= ? order by id desc";
    如果使用ps.setDate(1, java.sql.Date date); 来进行填充,那么java.sql.Date只有yyyy-mm-dd没有时间信息。并且我们数据库里的是java.Util.Date。需要进行转换:java.sql.Date(java.sql.Date d.getTime())。但转换过来就只有日期。
    也可以使用ps.setTimeStamp(1, java.sql.TimeStamp(d.getTime()))来进行填充,但是timeStamp是毫秒计数,转换出来的是yyyy-mm-dd HH:MM:SS.ms。同样不符合要求。所以我选择了用字符串来填充的方式:
    1
    2
    3
    4
    5
    public static String util2sqlTimestamp(java.util.Date d){
    DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String dateStr = sdf.format(d);
    return dateStr;
    }
  3. statement执行。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Statement s = connection.createStatement(); //这里不加sql
    s.execute(sql);
    s.executeQuery(sql);

    PreparedStatement = connection.preparedStatement(sql); //这里要加上sql
    ps.execute();
    ResultSet rs = ps.executeQuery();

    注意如果需要返回ResultSet需要在statement中加入Statement.RETURN_GENERATED_KEYS
  4. ResultSet返回
    1
    2
    3
    rs.next(); //用来判断是否返回ResultSet
    rs.getInt(); //填入列号或者字段名
    rs.getString();

    UI设计,Service与Listener触发

1. UI设计
UI设计主要用到包含两个库:

  • import javax.swing.JFrame; //用来创建主窗体,包含各种板面
  • import javax.swing.JPanel; //用来创建各种板面,包含各种控件。
    主窗体是整个程序的入口。一个程序的UI实际上就是在一个主窗体上不断变换各种显示面板。
    创建单例模式
    单例模式指的是在应用整个生命周期内只能存在一个实例。单例模式是一种被广泛使用的设计模式。他有很多好处,能够避免实例对象的重复创建,减少创建实例的系统开销,节省内存。
    单例模式与静态类的区别 :首先单例模式是一个唯一存在的实例。静态类只是在程序编译的时候就创建了,可以不用创建实例就能进行调用。如果是一个非常重的对象,单例模式可以进行懒加载,静态类无法做到。如果只想使用一些方法或者变量,使用静态类在编译的过程中进行构建会比较快;但如果这个对象需要大量的后期维护,访问资源的时候,应该选择单例模式。

这里创建主窗体和板面都我们都用了单例模式:(都使用了饿汉模式加载:声明静态变量,在编译的时候构建对象, 缺点是占用资源,这种方式适合占用资源少,在初始化时候就能够用到的类。)

1
2
3
4
5
6
7
8
9
10
11
12
主窗体单例模式
public class MainFrame extends JFrame{
public static MainFrame instance = new MainFrame();
private MainFrame() {
// TODO Auto-generated constructor stub
this.setSize(500, 450);
this.setTitle("一本糊涂账");
this.setContentPane(MainPanel.instance);
this.setLocationRelativeTo(null);//无固定位置
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
}

在考虑板面交互的过程中,我们发现板面拥有共同的特征:

  1. 基本都需要对按键等控件添加监听器触发
  2. 基本都要进行数据的更新操作
    因为我们抽象出一个WorkingPanel作为每一个板面的抽象类。注意继承抽象类的类必须实现所有的抽象类中定义为abstract的方法。
    1
    2
    3
    4
    public abstract class WorkingPanel extends JPanel{
    public abstract void updateData(); //添加数据更新方法
    public abstract void addListener(); //添加监听器
    }
    在显示板面的时候,首先要更新板面数据。然后使用JPanel.updateUI()来进行板面更新。

2. 控件监听器Listener
空间监听器是实现了ActionListener接口的类。需要实现actionPerformed方法,表示当该监听器被触发的时候做的一系列操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ConfigListener implements ActionListener{

/* (non-Javadoc)
* @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
*/
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
ConfigPanel panel = ConfigPanel.instance; //调用ConfigPanel面板,读取相应的数据
if(!GUIUtil.checkNumber(panel.budgetTextField, "本月预算")){
return;
}

String mysqlPath = panel.mysqlPathTextField.getText();
if(0 != mysqlPath.length()){
File commandFile = new File(mysqlPath, "bin/mysql");
if(!commandFile.exists()){
JOptionPane.showMessageDialog(panel, "Mysql路径不正确");
panel.mysqlPathTextField.grabFocus();
return;
}
}

//两个验证通过,连接Config服务
ConfigService cs = new ConfigService();
cs.update(ConfigService.budget, panel.budgetTextField.getText()); //更新预算数字
cs.update(ConfigService.mysqlPath, panel.mysqlPathTextField.getText()); //更新数据库安装目录
cs.update(ConfigService.backupPath, panel.backupFilePathTextField.getText()); //更新备份目录
JOptionPane.showMessageDialog(panel, "设置修改成功");
}
}

在板面的addListener方法中,通过如下方式添加监听器。

1
2
3
4
5
protected void addListener() {
// TODO Auto-generated method stub
ConfigListener listener = new ConfigListener();
updateButton.addActionListener(listener); //为updateButton控件添加事件监听器
}

一般情况下,在监听器被触发的条件下,都会产生相对应的操作。这些操作都会定义在Service类中,并放在监听器的actionPerformed方法中执行。
3. Service相应服务
对于本例来说,相应的服务都是对数据库进行相关操作,也就是数据更新,数据查询等操作。这个时候就会用到之前所定义的DAO类进行数据库相关操作。

Debug开发日志

2019-07-16 第一版构建完成

消费一览
记一笔
消费分类
月消费报表
设置
备份
恢复