1. 项目分析
在设计一款软件时,在编写代码之前,应该先分析这个项目中需要处理哪些类型的数据!例如,本项目中需要处理的数据种类有:收藏,购物车,用户,收货地址,订单,商品,商品类别。
当确定了需要处理的数据的种类之后,就应该确定这些数据的处理先后顺序:用户 > 收货地址 > 商品类别 > 商品 > 收藏 > 购物车 > 订单。
在具体开发某个数据的管理功能之前,还应该分析该数据需要开发哪些管理功能,以用户数据为例,需要开发的有:修改密码,上传头像,修改资料,登录,注册。
分析出功能之后,也需要确定这些功能的开发顺序,一般先开发简单的,也依据增、查、删、改的顺序,则以上功能的开发顺序应该是:注册 > 登录 > 修改密码 > 修改资料 > 上传头像。
在开发某个数据的任何功能之前,还应该先创建这种数据对应的数据表,然后,创建对应的实体类,再开发某个功能!
在开发某个功能时,还应该遵循顺序:持久层(数据库编程) > 业务层 > 控制器层 > 前端页面。
2. 用户-创建数据表
先创建数据库:
CREATE DATABASE db_store;
USE db_store;
然后,在数据库中创建数据表:
CREATE TABLE t_user (
uid INT AUTO_INCREMENT COMMENT '用户id',
username VARCHAR(20) UNIQUE NOT NULL COMMENT '用户名',
password CHAR(32) NOT NULL COMMENT '密码',
salt CHAR(36) COMMENT '盐值',
gender INT(1) COMMENT '性别:0-女,1-男',
phone VARCHAR(20) COMMENT '手机号码',
email VARCHAR(50) COMMENT '电子邮箱',
avatar VARCHAR(100) COMMENT '头像',
is_delete INT(1) COMMENT '是否删除:0-否,1-是',
created_user VARCHAR(20) COMMENT '创建人',
created_time DATETIME COMMENT '创建时间',
modified_user VARCHAR(20) COMMENT '最后修改人',
modified_time DATETIME COMMENT '最后修改时间',
PRIMARY KEY (uid)
) DEFAULT CHARSET=utf8mb4;
完成后,可以通过desc t_user;
和show create table t_user;
进行查看。
3. 用户-创建实体类
创建SpringBoot项目,所以,先打开https://start.spring.io
创建项目,创建时,使用的版本选择2.1.12
,Group为cn.demo,Artifact为
store,Packaging为
war,添加
Mybatis Framework和
MySQL Driver` 依赖,在网站生成项目后,将解压得到的项目文件夹剪切到Workspace中,并在Eclipse中导入该项目。
在src/main/java下,在现有的cn.demo.store
包中,创建子级entity
包,用于存放实体类,先在entity
包中创建所有实体类的基类:
/**
* 实体类的基类
*/
abstract class BaseEntity implements Serializable {
private static final long serialVersionUID = -3122958702938259476L;
private String createdUser;
private Date createdTime;
private String modifiedUser;
private Date modifiedTime;
// 自行添加SET/GET方法,toString()
}
并在entity
包中创建User
类,继承自以上基类:
/**
* 用户数据的实体类
*/
public class User extends BaseEntity {
private static final long serialVersionUID = -3302907460554699349L;
private Integer uid;
private String username;
private String password;
private String salt;
private Integer gender;
private String phone;
private String email;
private String avatar;
private Integer isDelete;
// 自行添加SET/GET方法,基于uid的equals()和hashCode()方法,toString()方法
}
4. 用户-注册-持久层
持久层:持久化保存数据的层。
刚创建好的SpringBoot项目,由于添加了数据库相关的依赖,在没有配置数据库连接信息之前,将无法启动!所以,应该先在application.properties中添加配置:
server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/db_store?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456
mybatis.mapper-locations=classpath:mappers/*.xml
然后,在cn.demo.store
包中,创建mapper
子级包,用于存放使用MyBatis编程时的接口文件,并在mapper
包中创建UserMapper
接口,在接口中添加抽象方法:
/**
* 处理用户数据的持久层接口
*/
public interface UserMapper {
/**
* 插入用户数据
* @param user 用户数据
* @return 受影响的行数
*/
Integer insert(User user);
/**
* 根据用户名查询用户数据
* @param username 用户名
* @return 匹配的用户数据,如果没有匹配的数据,则返回null
*/
User findByUsername(String username);
}
然后,需要在启动类的声明之前补充@MapperScan
注解,以配置接口文件的位置:
@SpringBootApplication
@MapperScan("cn.demo.store.mapper")
public class StoreApplication {
public static void main(String[] args) {
SpringApplication.run(StoreApplication.class, args);
}
}
在src/main/resources下创建mappers文件夹,该文件夹的名称应该与复制的配置信息中保持一致!并在该文件夹中创建UserMapper.xml文件,以配置2个抽象方法的SQL映射:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<mapper namespace="cn.demo.store.mapper.UserMapper">
<resultMap type="cn.demo.store.entity.User" id="UserEntityMap">
<id column="uid" property="uid"/>
<result column="is_delete" property="isDelete"/>
<result column="created_user" property="createdUser"/>
<result column="created_time" property="createdTime"/>
<result column="modified_user" property="modifiedUser"/>
<result column="modified_time" property="modifiedTime"/>
</resultMap>
<!-- 插入用户数据 -->
<!-- Integer insert(User user); -->
<insert id="insert" useGeneratedKeys="true" keyProperty="uid">
INSERT INTO t_user (
username, password, salt, gender,
phone, email, avatar, is_delete,
created_user, created_time, modified_user, modified_time
) VALUES (
#{username}, #{password}, #{salt}, #{gender},
#{phone}, #{email}, #{avatar}, #{isDelete},
#{createdUser}, #{createdTime}, #{modifiedUser}, #{modifiedTime}
)
</insert>
<!-- 根据用户名查询用户数据 -->
<!-- User findByUsername(String username) -->
<select id="findByUsername" resultMap="UserEntityMap">
SELECT * FROM t_user WHERE username=#{username}
</select>
</mapper>
在src/test/java中的cn.demo.store
包中创建子级的mapper
包,并在mapper
包中创建UserMapperTests
测试类,并在测试类的声明之前添加@RunWith(SpringRunner.class)
和@SpringBootTest
注解:
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserMapperTests {
}
如果使用的是SpringBoot 2.2.x系列的版本,只需要添加1个注解即可,具体使用什么样的注解,请参考默认就存在那个单元测试类。
然后,在单元测试类中编写并执行单元测试:
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserMapperTests {
@Autowired
private UserMapper mapper;
@Test
public void insert() {
User user = new User();
user.setUsername("project");
user.setPassword("1234");
user.setSalt("salt");
user.setGender(0);
user.setPhone("13800138002");
user.setEmail("project@163.com");
user.setAvatar("avatar");
user.setIsDelete(0);
user.setCreatedUser("系统管理员");
user.setCreatedTime(new Date());
user.setModifiedUser("超级管理员");
user.setModifiedTime(new Date());
Integer rows = mapper.insert(user);
System.err.println("rows=" + rows);
System.err.println(user);
}
@Test
public void findByUsername() {
String username = "project";
User result = mapper.findByUsername(username);
System.err.println(result);
}
}
5. 用户-注册-业务层
业务,在普通用户眼里就是“1个功能”,例如“注册”就是一个业务,在开发人员看来,它可能是由多个数据操作所组成的,例如“注册”就至少由“查询用户名对应的用户数据”和“插入用户数据”这2个数据操作组成,多个数据操作组成1个业务,在组织过程中,可能涉及一些相关的检查,及数据安全、数据完整性的保障,所以,业务层的代码主要是组织业务流程,设计业务逻辑,以保障数据的完整性和安全性。
在开发领域中,数据安全指的是:数据是由开发人员所设定的规则而产生或发生变化的!
在业务层的开发中,应该先创建业务层的接口,因为,在实际项目开发中,强烈推荐“使用接口编程”的效果!
所以,先在cn.demo.store
包中创建service
子包,并在service
包中创建UserService
业务接口,并在接口中声明“注册”这个业务的抽象方法:
/**
* 处理用户数据的业务接口
*/
public interface UserService {
/**
* 用户注册
* @param user 客户端提交的用户数据
*/
void reg(User user);
}
在设计抽象方法时,仅以操作成功(例如注册成功、登录成功等)为前提来设计抽象方法的返回值,涉及的操作失败将通过抛出异常来表示!
**创建异常处理:**在cn.demo.store
下创建ex
子包,并创建异常的父类(ServiceException)
由于需要使用异常来表示错误,所以,在实现抽象方法的功能之前,还应该先定义相关的异常,有哪些“错误”(导致操作失败的原因),就创建哪些异常类,例如,注册时,用户名可能已经被占用,则需要创建对应的异常,当用户名没有被占用,允许注册时,执行的INSERT操作也可能失败,导致相应的异常,为了便于统一管理这些异常,还应该创建自定义异常的基类,这个基类异常应该继承自RuntimeException
:
/**
* 业务异常的基类
*/
public class ServiceException extends RuntimeException {
private static final long serialVersionUID = 980104530291206274L;
public ServiceException() {
super();
}
public ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public ServiceException(String message, Throwable cause) {
super(message, cause);
}
public ServiceException(String message) {
super(message);
}
public ServiceException(Throwable cause) {
super(cause);
}
}
-----------------------------------------------------------------------------
/**
* 用户名冲突的异常
*/
public class UsernameDuplicateException extends ServiceException {
private static final long serialVersionUID = -1224474172375139228L;
public UsernameDuplicateException() {
super();
}
public UsernameDuplicateException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public UsernameDuplicateException(String message, Throwable cause) {
super(message, cause);
}
public UsernameDuplicateException(String message) {
super(message);
}
public UsernameDuplicateException(Throwable cause) {
super(cause);
}
}
-------------------------------------------------------------------------------
/**
* 插入数据异常
*/
public class InsertException extends ServiceException {
private static final long serialVersionUID = 7991875652328476596L;
public InsertException() {
super();
}
public InsertException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public InsertException(String message, Throwable cause) {
super(message, cause);
}
public InsertException(String message) {
super(message);
}
public InsertException(Throwable cause) {
super(cause);
}
}
接下来,就需要编写接口的实现类,并实现接口中的抽象方法!所以,在cn.demo.store.service
包创建子级的impl
包,并在impl
包中创建UserServiceImpl
类,实现UserService
接口,在类的声明之前添加@Service
注解,使得Spring框架能够创建并管理这个类的对象!并且,由于在实现过程中,必然用到持久层开发的数据操作,所以,还应该声明UserMapper
对象,该对象的值应该是自动装配的:
/**
* 处理用户数据的业务层实现类
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public void reg(User user) {
}
}
接下来,分析实现过程:
public void reg(User user) {
// 通过参数user获取尝试注册的用户名
String username = user.getUsername();
// 调用userMapper.findByUsername()方法执行查询
User result = userMapper.findByUsername(username);
// 判断查询结果是否不为null
if (result != null) {
// 是:查询到了数据,表示用户名已经被占用,则抛出UsernameDuplicationException
throw new UsernameDuplicateException();
}
// 如果代码能执行到这一行,则表示没有查到数据,表示用户名未被注册,则允许注册
// 创建当前时间对象:
Date now = new Date();
// 向参数user中补全数据:salt, password,涉及加密处理,暂不处理
// 向参数user中补全数据:is_delete(0)
user.setIsDelete(0);
// 向参数user中补全数据:4项日志(now, user.getUsername())
user.setCreaser(username);
user.setCreatedTime(now);
user.setModifiedUser(username);
user.setModifiedTime(now);
// 调用userMapper.insert()执行插入数据,并获取返回的受影响行数
Integer rows = userMapper.insert(user);
// 判断受影响的行数是否不为1
if (rows != 1) {
// 是:插入数据失败,则抛出InsertException
throw new InsertException();
}
}
然后,应该在src/test/java下的cn.demo.store
包中创建子级的service
包,并在这个包中创建UserServiceTests
测试类,专门用于测试UserService
接口中定义的功能:
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTests {
@Autowired
private UserService service;
@Test
public void reg() {
try {
User user = new User();
user.setUsername("service");
user.setPassword("1234");
user.setGender(0);
user.setPhone("13800138003");
user.setEmail("service@163.com");
user.setAvatar("avatar");
service.reg(user);
System.err.println("OK.");
} catch (ServiceException e) {
System.err.println(e.getClass().getName());
}
}
}
最后,还应该处理密码加密(添加commons-codec依赖),完整业务代码例如:
/**
* 处理用户数据的业务层实现类
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public void reg(User user) {
// 日志
System.err.println("UserServiceImpl.reg()");
// 通过参数user获取尝试注册的用户名
String username = user.getUsername();
// 调用userMapper.findByUsername()方法执行查询
User result = userMapper.findByUsername(username);
// 判断查询结果是否不为null
if (result != null) {
// 是:查询到了数据,表示用户名已经被占用,则抛出UsernameDuplicationException
throw new UsernameDuplicateException();
}
// 如果代码能执行到这一行,则表示没有查到数据,表示用户名未被注册,则允许注册
// 创建当前时间对象:
Date now = new Date();
// 向参数user中补全数据:salt, password
String salt = UUID.randomUUID().toString();
user.setSalt(salt);
String md5Password = getMd5Password(user.getPassword(), salt);
user.setPassword(md5Password);
// 向参数user中补全数据:is_delete(0)
user.setIsDelete(0);
// 向参数user中补全数据:4项日志(now, user.getUsername())
user.setCr