首先应该分析该项目中需要处理哪些种类的数据,在本项目中有:商品、商品类别、用户、收货地址、购物车、收藏、订单……
然后,确定以上这些数据的开发顺序,原则上应该先开发基础数据和简单的数据相关的功能,所以,以上数据的开发顺序应该是:用户 > 收货地址 > 商品类别 > 商品 > 收藏 > 购物车 > 订单。
接下来,分析第1种数据的相关功能,即“用户”数据的管理中,有哪些功能需要开发:注册,登录,修改密码,个人资料,上传头像。
分析完成后,再确定以上功能的开发顺序,通常开发顺序的基本原则是:增、查、删、改,所以,以上功能的开发顺序应该是:注册 > 登录 > 修改密码 > 个人资料 > 上传头像。
在具体的开发某个功能时,应该遵循的顺序是:创建数据表 > 创建实体类 > 持久层 > 业务层 > 控制器层 > 前端界面。
首先应该创建数据库:
CREATE DATABASE tedu_store;
然后使用该数据库:
USE tedu_store
再创建数据表:
CREATE TABLE t_user (
uid INT AUTO_INCREMENT COMMENT '用户id',
username VARCHAR(50) UNIQUE NOT NULL COMMENT '用户名',
password CHAR(32) NOT NULL COMMENT '密码',
salt CHAR(36) COMMENT '盐值',
gender INT COMMENT '性别,0-女,1-男',
phone VARCHAR(20) COMMENT '手机号码',
email VARCHAR(50) COMMENT '电子邮箱',
avatar VARCHAR(100) COMMENT '头像',
is_delete INT COMMENT '是否删除,0-未删除,1-已删除',
created_user VARCHAR(50) COMMENT '创建人',
created_time DATETIME COMMENT '创建时间',
modified_user VARCHAR(50) COMMENT '最后修改人',
modified_time DATETIME COMMENT '最后修改时间',
PRIMARY KEY (uid)
) DEFAULT CHARSET=utf8;
先下载共享的项目文件,解压得到项目文件夹,将其移动到Workspace中,通过Import中的Existing Maven Projects导入项目,然后,在此前的springboot项目中复制数据库连接的配置信息到新项目中,并修改需要连接到的数据库名称为tedu_store。
然后,在src/test/java下的测试类,先执行原有的空的测试方法,以检验环境是否正确,然后,在该测试类测试获取数据库连接对象,以检验连接配置信息是否正确:
@Autowired
DataSource dataSource;
@Test
public void getConnection() throws SQLException {
Connection conn = dataSource.getConnection();
System.err.println(conn);
}
然后,先创建cn.tedu.store.entity.BaseEntity
实体的父类,称之为实体类的“基类”,实现序列化接口,该类的作用就是用于被继承的,所以应该添加abstract
修饰符,并且,该类也只需要被子类访问,子类都与它在同一个包中,所以,使用默认的访问权限即可:
abstract class BaseEntity implements Serializable {
private String createdUser;
private Date createdTime;
private String modifiedUser;
private Date modifiedTime;
}
再cn.tedu.store.entity.User
实体类,继承自以上BaseEntity
类:
public class User extends BaseEntity {
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生成hashCode和equals/Serializable
}
(a) 规划SQL语句
首先,应该分析该功能需要执行的SQL语句,当用户注册时,本质是向数据表中插入数据,则需要执行SQL语句大致是:
insert into t_user (
除了uid以外的所有字段
) values (
?,?,?,?,?...?
);
由于设计了“用户名唯一”的规则,在插入数据之前,还应该检查该用户名是否已经被占用,则可以“根据用户名查询数据,并判断是否查询到有效结果”作为判断依据,需要执行的SQL语句大致是:
select uid from t_user where username=?
(b) 接口与抽象方法
创建cn.tedu.store.mapper.UserMapper
接口,并在接口中添加抽象方法:
Integer addnew(User user);
User findByUsername(String username);
注意:完成后,应该在启动类之前添加
@MapperScan("cn.tedu.store.mapper")
注解,以配置MyBatis中的持久层接口在哪个包中。
© 配置映射
在src/main/resources下创建mappers文件夹,并在该文件夹中粘贴持久层映射的XML文件,确定文件名应该是UserMapper.xml,配置好根节点的namespace
属性,并配置以上接口中2个抽象方法的映射:
<mapper namespace="cn.tedu.store.mapper.UserMapper">
<insert id="addnew"
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>
<select id="findByUsername"
resultType="cn.tedu.store.entity.User">
SELECT
uid
FROM
t_user
WHERE
username=#{username}
select>
mapper>
注意:需要检查在application.properties中是否配置了XML映射文件的位置mybatis.mapper-locations=classpath:/mappers/*.xml
完成后,在src/test/java下创建cn.tedu.store.mapper.UserMapperTests
单元测试类,并在该类中编写并执行持久层2个功能的测试方法:
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserMapperTests {
@Autowired
UserMapper mapper;
@Test
public void addnew() {
User user = new User();
user.setUsername("admin");
user.setPassword("1234");
System.err.println(user);
Integer rows = mapper.addnew(user);
System.err.println("rows=" + rows);
System.err.println(user);
}
@Test
public void findByUsername() {
String username = "root888";
User result = mapper.findByUsername(username);
System.err.println(result);
}
}
(a) 规划异常
首先,应该分析用户在执行此次操作时,可能有哪些失败的原因。
应该为所有的自定义异常创建一个公共的父类,以确定自定义异常的类别,所以,创建cn.tedu.store.service.ex.ServiceException
,它应该继承自RuntimeException
。
在当前项目中,设置了“用户名唯一”的规则,如果用户尝试注册的用户名已经被占用,则应该抛出自定义的cn.tedu.store.service.ex.UsernameDuplicateException
异常,继承自ServiceException
。
此次“注册”将执行INSERT
语句,则可能存在某些不控的因素导致插入失败,所以,还应该为这种失败的可能设计对应的cn.tedu.store.service.ex.InsertException
异常。
(b) 接口与抽象方法
创建cn.tedu.store.service.IUserService
业务层接口,并在接口中添加抽象方法:
void reg(User user) throws UsernameDuplicateException, InsertException;
在设计抽象方法时,方法的返回值仅以操作成功为前提进行设计,如果需要考虑操作失败,则使用抛出异常的方式来解决。
© 实现抽象方法
创建cn.tedu.store.service.impl.UserServiceImpl
业务层实现类,并实现以上接口,在类之前添加@Service
注解,在类中添加@Autowired private UserMapper userMapper;
持久层对象:
@Service
public class UserServiceImpl implements IUserService {
@Autowired
private UserMapper userMapper;
public void reg(User user) {
}
}
然后,重写抽象方法:
public void reg(User user) {
// 根据参数user中的getUsername()获取尝试注册的用户名
// 根据以上用户名查询用户数据
// 判断查询结果是否不为null
// 是:用户名已经被占用,抛出UsernameDuplicateException
// 用户名未被占用,允许注册
// TODO 向参数user中补全属性:盐值
// TODO 取出参数user中的原始密码
// TODO 将原始密码加密
// TODO 向参数user中补全属性:加密后的密码
// 向参数user中补全属性:isDelete-0
// 向参数user中补全属性:4项日志
// 执行注册
}
初步实现为:
@Override
public void reg(User user) throws UsernameDuplicateException, InsertException {
// 根据参数user中的getUsername()获取尝试注册的用户名
String username = user.getUsername();
// 根据以上用户名查询用户数据
User result = userMapper.findByUsername(username);
// 判断查询结果是否不为null
if (result != null) {
// 是:用户名已经被占用,抛出UsernameDuplicateException
throw new UsernameDuplicateException(
"注册失败!尝试注册的用户名(" + username + ")已经被占用!");
}
// 用户名未被占用,允许注册
// TODO 向参数user中补全属性:盐值
// TODO 取出参数user中的原始密码
// TODO 将原始密码加密
// TODO 向参数user中补全属性:加密后的密码
// 向参数user中补全属性:isDelete-0
user.setIsDelete(0);
// 向参数user中补全属性:4项日志
Date now = new Date();
user.setCreatedUser(username);
user.setCreatedTime(now);
user.setModifiedUser(username);
user.setModifiedTime(now);
// 执行注册
Integer rows = userMapper.addnew(user);
if (rows != 1) {
throw new InsertException(
"注册失败!插入用户数据时出现未知错误!请联系管理员!");
}
}
完成后,在src/test/java下创建cn.tedu.store.service.UserServiceTests
单元测试类,并在该类中编写并执行以上功能的测试方法:
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTests {
@Autowired
IUserService service;
@Test
public void reg() {
try {
User user = new User();
user.setUsername("Service");
user.setPassword("1234");
service.reg(user);
System.err.println("OK");
} catch (ServiceException e) {
System.err.println(e.getClass().getName());
System.err.println(e.getMessage());
}
}
}
(a) 统一处理异常
创建cn.tedu.store.controller.BaseController
控制类的基类,后续创建的每个控制器类都应该继承自这个基类,在基类中添加处理处理异常的方法,则每个子级的控制器类都相当于拥有这个方法:
public abstract class BaseController {
@ExceptionHandler(ServiceException.class)
@ResponseBody
public JsonResult<Void> handleException(Throwable e) {
JsonResult<Void> jr = new JsonResult<Void>();
jr.setMessage(e.getMessage());
if (e instanceof UsernameDuplicateException) {
jr.setState(2);
} else if (e instanceof InsertException) {
jr.setState(3);
}
return jr;
}
}
(b) 设计请求
设计“用户注册”的请求方式:
请求路径:/users/reg
请求参数:User user
请求方式:POST
响应数据:JsonResult
© 处理请求
首先,需要创建cn.tedu.store.util.JsonResult
响应结果类型:
public class JsonResult<T> {
private Integer state;
private String message;
private T data;
// SET/GET
}
再需要创建cn.tedu.store.controller.UserController
控制器类,在类之前添加@RestController
和@RequestMapping("users")
这2个注解,在类中添加声明@Autowired private IUserService userService;
业务层对象:
@RestController
@RequestMapping("users")
public class UserController extends xxxController {
@Autowired
private IUserService userService;
}
接下来,在类中添加处理请求的方法:
@RequestMapping("reg")
public JsonResult<Void> reg(User user) {
JsonResult<Void> jr = new JsonResult<Void>();
userService.reg(user);
jr.setState(1);
return jr;
}
完成后,启动项目,打开浏览器,通过http://localhost:8080/users/reg?username=root&password=1234
进行测试。
测试完成后,将方法之前的@RequestMapping
替换为@PostMapping
。
(a) 规划SQL语句
用户登录时,需要检查用户名是否存在,密码是否匹配,且is_delete
的值是否表示“未删除”,关于“用户名”是否存在,可以通过查询数据库得到:
select * from t_user where username=?
如果查询到有效结果,则用户名是存在,可以进行后续的验证,如果查询的结果是null
,则用户名尚未注册,不允许使用这个用户名登录!
关于密码、is_delete
的验证应该在后续的业务层中进行判断,只需要保证以上查询时,能够查到与验证相关的字段即可,例如:
select password, salt, is_delete from t_user where username=?
并且,如果用户登录成功,还应该将用户(客户端)提供用户的相关数据,例如用户的用户名、头像,还需要在Session中记录该用户的id和用户名,则查询时还要查询这些字段:
select uid, username, password, salt, avatar, is_delete from t_user where username=?
(b) 接口与抽象方法
在UserMapper.java
接口中已经存在User findByUsername(String username)
方法,则无需重复定义抽象方法。
© 配置映射
此次需要在findByUsername()
方法的映射中补充查询更多的字段:
完成后,再次执行已经存在的单元测试。
(a) 规划异常
此时,应该穷举用户在“登录”过程中可能出现的任何“错误”,例如,可能出现“用户名不存在”,或“密码错误”,或“用户数据被标记为已删除”。
所以,需要为以上“错误”创建对应的异常类:
cn.tedu.store.service.ex.UserNotFoundException
cn.tedu.store.service.ex.PasswordNotMatchException
创建的异常类都应该继承自ServiceException
。
(b) 接口与抽象方法
在IUserService
接口中添加“登录”的抽象方法:
User login(String username, String password) throws UserNotFoundException, PasswordNotMatchException;
© 实现抽象方法
在UserServiceImpl
实现类中重写以上抽象方法:
public User login(String username, String password) throws UserNotFoundException, PasswordNotMatchException {
// 根据参数username执行查询
// 判断查询结果是否为null
// 是:抛出UserNotFoundException
// 判断查询结果中的isDelete是否为1
// 是:抛出UserNotFoundException
// 从查询结果中获取盐值
// 基于参数password和盐值执行加密
// 判断以上加密结果与查询结果中的password是否不匹配
// 是:抛出PasswordNotMatchException
// 将查询结果中的password设置为null
// 将查询结果中的salt设置为null
// 将查询结果中的isDelete设置为null
// 返回查询结果
}
具体实现为:
@Override
public User login(String username, String password) throws UserNotFoundException, PasswordNotMatchException {
// 根据参数username执行查询
User result = userMapper.findByUsername(username);
// 判断查询结果是否为null
if (result == null) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException(
"登录失败!用户数据不存在!");
}
// 判断查询结果中的isDelete是否为1
if (result.getIsDelete() == 1) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException(
"登录失败!用户数据不存在!");
}
// 从查询结果中获取盐值
String salt = result.getSalt();
// 基于参数password和盐值执行加密
String md5Password = getMd5Password(password, salt);
// 判断以上加密结果与查询结果中的password是否不匹配
if (!md5Password.equals(result.getPassword())) {
// 是:抛出PasswordNotMatchException
throw new PasswordNotMatchException(
"登录失败!密码错误!");
}
// 将查询结果中的password设置为null
result.setPassword(null);
// 将查询结果中的salt设置为null
result.setSalt(null);
// 将查询结果中的isDelete设置为null
result.setIsDelete(null);
// 返回查询结果
return result;
}
最后,在UserServiceTests
中编写并执行单元测试:
@Test
public void login() {
try {
String username = "root";
String password = "1234x";
User result = service.login(username, password);
System.err.println(result);
} catch (ServiceException e) {
System.err.println(e.getClass().getName());
System.err.println(e.getMessage());
}
}
(a) 统一处理异常
在BaseController
的处理异常的方法中,添加更多的分支,对以上新抛出的2种异常进行处理:
if (e instanceof UsernameDuplicateException) {
jr.setState(4000);
} else if (e instanceof UserNotFoundException) {
jr.setState(4001);
} else if (e instanceof PasswordNotMatchException) {
jr.setState(4002);
} else if (e instanceof InsertException) {
jr.setState(5000);
}
(b) 设计请求
设计“用户登录”的请求方式:
请求路径:/users/login
请求参数:String username, String password, HttpSession session
请求方式:POST
响应数据:JsonResult
© 处理请求
在类中添加处理请求的方法:
@RequestMapping("login")
public JsonResult login(String username, String password, HttpSession session) {
// 调用业务层对象的“登录”方法,获取返回结果
// 向Session中存入用户id和用户名
// 返回
}
具体代码为:
@RequestMapping("login")
public JsonResult<User> login(String username, String password, HttpSession session) {
// 调用业务层对象的“登录”方法,获取返回结果
User data = userService.login(username, password);
// 向Session中存入用户id和用户名
session.setAttribute("uid", data.getUid());
session.setAttribute("username", data.getUsername());
// 返回
return new JsonResult<>(SUCCESS, data);
}
完成后,可以通过http://localhost:8080/users/login?username=rootx&password=1234x
进行单元测试,完成后,将@RequestMapping
改成@PostMapping
。
对于输出的JSON结果过于复杂,且将为null的结果都已经输出为JSON数据,这种做法并不合理,存在浪费资源的问题,并且还暴露项目的特征数据,要解决该问题,可以在application.properties中添加配置,使得为null的属性将不被输出到JSON结果中:
spring.jackson.default-property-inclusion=non_null
(a) 规划SQL语句
执行修改密码的SQL语句大致是:
update t_user set password=?, modified_user=?, modified_time=? where uid=?
在执行修改之前,还需要验证原密码是否正确,但是,不应该是:
update t_user set password=? where uid=? and password=?
之所以不将密码作为查询条件之一,是因为在SQL语句中不区分大小写,而密码应该匹分大小写,另外,“需要验证原密码”是软件开发者所设计的业务规则,应该在业务层中去体现,并不应该写在SQL语句中。
为了保证“验证原密码”的功能,还需要查询出原密码和盐值,则需要执行:
select password, salt from t_user where uid=?
另外,在查询时,还应该检查用户的is_delete是否正常,所以:
select password, salt, is_delete from t_user where uid=?
(b) 接口与抽象方法
在UserMapper.java
接口中添加抽象方法:
Integer updatePassword(
@Param("uid") Integer uid,
@Param("password") String password,
@Param("modifiedUser") String modifiedUser,
@Param("modifiedTime") Date modifiedTime);
User findByUid(Integer uid);
© 配置映射
在UserMapper.xml
中配置以上2个抽象方法的映射,如果不希望每次查询时都需要自定义别名,可以事先配置好
:
<resultMap id="UserEntityMap"
type="cn.tedu.store.entity.User">
<id column="uid" property="uid"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="salt" property="salt"/>
<result column="gender" property="gender"/>
<result column="phone" property="phone"/>
<result column="email" property="email"/>
<result column="avatar" property="avatar"/>
<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>
<update id="updatePassword">
UPDATE
t_user
SET
password=#{password},
modified_user=#{modifiedUser},
modified_time=#{modifiedTime}
WHERE
uid=#{uid}
update>
<select id="findByUid"
resultMap="UserEntityMap">
SELECT
password, salt,
is_delete
FROM
t_user
WHERE
uid=#{uid}
select>
在UserMapperTests
中编写并执行单元测试:
@Test
public void updatePassword() {
Integer uid = 8;
String password ="1234";
String modifiedUser = "超级管理员";
Date modifiedTime = new Date();
Integer rows = mapper.updatePassword(uid, password, modifiedUser, modifiedTime);
System.err.println("rows=" + rows);
}
@Test
public void findByUid() {
Integer uid = 8;
User result = mapper.findByUid(uid);
System.err.println(result);
}
(a) 规划异常
本次执行的主要是更新数据的操作,则可能出现UpdateException
。
在执行更新之前,还需要根据uid查询用户数据,检查数据是否存在,检查数据的is_delete,都可能出现UserNotFoundException
。
在执行更新之前,还需要验证原密码是否正确,则可能出现PasswordNotMatchException
。
则需要创建cn.tedu.store.service.ex.UpdateException
,并继承自ServiceException
。
(b) 接口与抽象方法
在IUserService
中添加“修改密码”的抽象方法:
void changePassword(Integer uid, String oldPassword, String newPassword, String modifiedUser) throws UserNotFoundException, PasswordNotMatchException, UpdateException;
关于抽象方法的参数的设计原则:要么是客户端提交的,要么是服务器端的控制器中提供的,并且,穷举所有参数后,足以调用持久层的相关功能!
© 实现抽象方法
在UserServiceImpl
实现类中添加新的抽象方法并实现:
public void changePassword(Integer uid, String oldPassword, String newPassword, String modifiedUser) throws UserNotFoundException, PasswordNotMatchException, UpdateException {
// 根据参数uid查询用户数据
// 判断查询结果是否为null:UserNotFoundException
// 判断查询结果中的isDelete是否为1:UserNotFoundException
// 从查询结果中获取盐值
// 对参数oldPassword执行加密,得到oldMd5Password
// 判断查询结果中的密码与oldMd5Password是否不匹配:PasswordNotMatchException
// 对参数newPassword执行加密,得到newMd5Passowrd
// 执行更新,获取返回值(受影响的行数)
// 判断受影响的行数是否不为1:UpdateException
}
具体实现为:
@Override
public void changePassword(Integer uid, String oldPassword, String newPassword, String modifiedUser)
throws UserNotFoundException, PasswordNotMatchException, UpdateException {
// 根据参数uid查询用户数据
User result = userMapper.findByUid(uid);
// 判断查询结果是否为null:UserNotFoundException
if (result == null) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException(
"修改密码失败!用户数据不存在!");
}
// 判断查询结果中的isDelete是否为1:UserNotFoundException
if (result.getIsDelete() == 1) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException(
"修改密码失败!用户数据不存在!");
}
// 从查询结果中获取盐值
String salt = result.getSalt();
// 对参数oldPassword执行加密,得到oldMd5Password
String oldMd5Password = getMd5Password(oldPassword, salt);
// 判断查询结果中的密码与oldMd5Password是否不匹配:PasswordNotMatchException
if (!result.getPassword().equals(oldMd5Password)) {
// 是:抛出PasswordNotMatchException
throw new PasswordNotMatchException(
"修改密码失败!原密码错误!");
}
// 对参数newPassword执行加密,得到newMd5Passowrd
String newMd5Password = getMd5Password(newPassword, salt);
// 执行更新,获取返回值(受影响的行数)
Integer rows = userMapper.updatePassword(uid, newMd5Password, modifiedUser, new Date());
// 判断受影响的行数是否不为1:UpdateException
if (rows != 1) {
throw new UpdateException(
"修改密码失败!更新数据时出现未知错误!");
}
}
最后,在UserServiceTests
中编写并执行单元测试:
@Test
public void changePassword() {
try {
Integer uid = 9;
String oldPassword = "8888";
String newPassword = "1234";
String modifiedUser = "系统管理员";
service.changePassword(uid, oldPassword, newPassword, modifiedUser);
System.err.println("OK");
} catch (ServiceException e) {
System.err.println(e.getClass().getName());
System.err.println(e.getMessage());
}
}
(a) 统一处理异常
需要在BaseController
中添加对UpdateException
的处理。
(b) 设计请求
设计“修改密码”的请求方式:
请求路径:/users/change_password
请求参数:@RequestParam("old_password") String oldPassword, @RequestParam("new_password") String newPassword, HttpSession session
请求方式:POST
响应数据:JsonResult
© 处理请求
在BaseController
中添加各子级控制器都可能需要执行的方法,例如获取uid的方法:
protected final Integer getUidFromSession(HttpSession session) {
return Integer.valueOf(session.getAttribute("uid").toString());
}
protected final String getUsernameFromSession(HttpSession session) {
return session.getAttribute("username").toString();
}
在UserController
中添加处理请求的方法:
@RequestMapping("change_password")
public JsonResult<Void> changePassword(
@RequestParam("old_password") String oldPassword,
@RequestParam("new_password") String newPassword,
HttpSession session) {
// 从session中获取uid
Integer uid = getUidFromSession(session);
// 从session中获取username
String username = getUsernameFromSession(session);
// 调用service对象执行修改密码
userService.changePassword(uid, oldPassword, newPassword, username);
// 响应成功
return new JsonResult<>(SUCCESS);
}
完成后,打开浏览器,先登录,然后通过http://localhost:8080/users/change_password?old_password=1234&new_password=8888
进行测试。
由于后续的越来越多的操作都是需要事先登录的,不登录则不允许执行相关操作,例如修改密码、修改资料、上传头像、创建收货地址、操作购物车、生成订单等……所以,可以在项目中添加登录拦截器,对用户是否登录进行验证,如果没有登录,则不执行后续的请求处理,而是直接重定向到登录页,避免发生错误!
首先,创建cn.tedu.store.interceptor.LoginInterceptor
拦截器类,并定义拦截处理方式:
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 获取HttpSession对象
HttpSession session = request.getSession();
// 判断session中是否有登录信息
if (session.getAttribute("uid") == null) {
// 没有登录信息,则重定向到登录页
response.sendRedirect("/web/login.html");
// 执行拦截
return false;
}
// 放行
return true;
}
}
然后,需要对拦截器进行配置,在SpringBoot中,需要自定义配置类,以对拦截器进行配置:
@Configuration
public class InterceptorConfigurer implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 创建拦截器对象
HandlerInterceptor interceptor = new LoginInterceptor();
// 白名单
List<String> patterns = new ArrayList<>();
patterns.add("/bootstrap3/**");
patterns.add("/css/**");
patterns.add("/js/**");
patterns.add("/images/**");
patterns.add("/web/register.html");
patterns.add("/web/login.html");
patterns.add("/users/reg");
patterns.add("/users/login");
// 注册拦截器
registry.addInterceptor(interceptor)
.addPathPatterns("/**")
.excludePathPatterns(patterns);
}
}
(a) 规划SQL语句
执行修改资料时需要执行的SQL语句大致是:
update t_user set phone=?,email=?,gender=?,modified_user=?,modified_time=? where uid=?
在执行修改之前,还是需要验证用户数据是否存在、是否标记为删除,该功能对应的SQL语句在“修改密码”时已经完成。
另外,在刚刚打开页面时,还需要显示当前登录的用户的信息,则需要根据用户的uid查询相关需要显示的信息:
select username,phone,email,gender from t_user where uid=?
(b) 接口与抽象方法
在UserMapper.java
中添加抽象方法:
Integer updateInfo(User user);
关于查询的功能在“修改密码”时已经存在User findByUid(Integer uid)
方法,则无需重复声明。
© 配置映射
在UserMapper.xml
配置Integer updateInfo(User user)
的映射:
<update id="updateInfo">
UPDATE
t_user
SET
phone=#{phone},
email=#{email},
gender=#{gender},
modified_user=#{modifiedUser},
modified_time=#{modifiedTime}
WHERE
uid=#{uid}
update>
然后,找到User findByUid(Integer uid)
的映射,补充查询字段:
<select id="findByUid"
resultMap="UserEntityMap">
SELECT
username, phone,
email, gender,
password, salt,
is_delete
FROM
t_user
WHERE
uid=#{uid}
select>
完成后,编写并执行“更新密码”的单元测试:
@Test
public void updateInfo() {
User user = new User();
user.setUid(7);
user.setPhone("13700137001");
user.setEmail("[email protected]");
user.setGender(1);
Integer rows = mapper.updateInfo(user);
System.err.println("rows=" + rows);
}
并再次执行测试“根据用户id查询信息”的功能。
(a) 规划异常
更新操作必然可能出现UpdateException
,在执行更新之前,应该检查用户数据,则可能出现UserNotFoundexception
。
(b) 接口与抽象方法
在IUserService
中添加抽象方法:
void changeInfo(Integer uid, String username, User user) throws UserNotFoundException, UpdateException;
User getByUid(Integer uid);
© 实现抽象方法
在UserServiceImpl
中实现以上抽象方法:
public void changeInfo(Integer uid, String username, User user) throws UserNotFoundException, UpdateException {
// 根据参数uid查询用户数据
// 判断查询结果是否为null:UserNotFoundException
// 判断查询结果中的isDelete是否为1:UserNotFoundException
// 将参数uid和参数username封装到参数user的uid和modifiedUser属性中
// 执行更新,获取返回值(受影响的行数)
// 判断受影响的行数是否不为1:UpdateException
}
public User getByUid(Integer uid) {
// 根据参数uid查询用户数据
// 判断查询结果是否为null:UserNotFoundException
// 判断查询结果中的isDelete是否为1:UserNotFoundException
// 将查询结果中不相关的数据设置为null
// 返回查询结果
}
具体实现为:
@Override
public void changeInfo(Integer uid, String username, User user) throws UserNotFoundException, UpdateException {
// 根据参数uid查询用户数据
User result = userMapper.findByUid(uid);
// 判断查询结果是否为null:UserNotFoundException
if (result == null) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException(
"修改个人资料失败!用户数据不存在!");
}
// 判断查询结果中的isDelete是否为1:UserNotFoundException
if (result.getIsDelete() == 1) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException(
"修改个人资料失败!用户数据不存在!");
}
// 将参数uid和参数username封装到参数user的uid和modifiedUser属性中
user.setUid(uid);
user.setModifiedUser(username);
user.setModifiedTime(new Date());
// 执行更新,获取返回值(受影响的行数)
Integer rows = userMapper.updateInfo(user);
// 判断受影响的行数是否不为1:UpdateException
if (rows != 1) {
throw new UpdateException(
"修改个人资料失败!更新数据时出现未知错误!");
}
}
@Override
public User getByUid(Integer uid) throws UserNotFoundException {
// 根据参数uid查询用户数据
User result = userMapper.findByUid(uid);
// 判断查询结果是否为null:UserNotFoundException
if (result == null) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException(
"获取用户资料失败!用户数据不存在!");
}
// 判断查询结果中的isDelete是否为1:UserNotFoundException
if (result.getIsDelete() == 1) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException(
"获取用户资料失败!用户数据不存在!");
}
// 将查询结果中不相关的数据设置为null
User user = new User();
user.setUsername(result.getUsername());
user.setPhone(result.getPhone());
user.setEmail(result.getEmail());
user.setGender(result.getGender());
// 返回查询结果
return user;
}
最后,在UserServiceTests
中编写并执行单元测试:
@Test
public void changeInfo() {
try {
Integer uid = 700;
String username = "系统管理员";
User user = new User();
user.setGender(1);
user.setPhone("13700137007");
user.setEmail("[email protected]");
service.changeInfo(uid, username, user);
System.err.println("OK");
} catch (ServiceException e) {
System.err.println(e.getClass().getName());
System.err.println(e.getMessage());
}
}
@Test
public void getByUid() {
try {
Integer uid = 7;
User data = service.getByUid(uid);
System.err.println(data);
} catch (ServiceException e) {
System.err.println(e.getClass().getName());
System.err.println(e.getMessage());
}
}
(a) 统一处理异常
没有新的异常,则无需处理。
(b) 设计请求
设计“获取用户数据”的请求方式:
请求路径:/users/get_by_uid
请求参数:HttpSession session
请求方式:GET
响应数据:JsonResult
设计“执行更新用户个人资料”的请求方式:
请求路径:/users/change_info
请求参数:User user, HttpSession session
请求方式:POST
响应数据:JsonResult
© 处理请求
在UserController
中:
@RequestMapping("change_info")
public JsonResult changeInfo(User user, HttpSession session) {
// 从session中获取uid和username
// 执行修改
// 返回成功
}
@GetMapping("get_by_uid")
public JsonResult getByUid(HttpSession session) {
// 执行获取数据
// 返回成功与数据
}
具体实现为:
@RequestMapping("change_info")
public JsonResult<Void> changeInfo(User user, HttpSession session) {
// 从session中获取uid和username
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
// 执行修改
userService.changeInfo(uid, username, user);
// 返回成功
return new JsonResult<>(SUCCESS);
}
@GetMapping("get_by_uid")
public JsonResult<User> getByUid(HttpSession session) {
// 执行获取数据
Integer uid = getUidFromSession(session);
User data = userService.getByUid(uid);
// 返回成功与数据
return new JsonResult<>(SUCCESS, data);
}
完成后,可以执行测试:
http://localhost:8080/users/get_by_uid
http://localhost:8080/users/change_info?gender=1&phone=111&email=222
在页面中点击浏览按钮,可以将指定的文件上传到服务器。
创建Maven Project,Group Id为cn.tedu.spring
,Artifact Id为SPRINGMVC-UPLOAD
,Packaging为war
,创建好项目后,生成web.xml文件,添加spring-webmvc
依赖,添加Tomcat运行环境(必须保证Eclipse中的Servers项目是打开的),复制spring的配置文件,删除所有已经存在的配置,复制前序项目中web.xml中的配置。
另外,为了实现文件上传,还需要添加依赖:
commons-fileupload
commons-fileupload
1.4
在webapp下创建index.html
页面,并在页面中设计:
以上代码中,的
method
属性值必须是post
,enctype
属性值必须是multipart/form-data
,且上传控件的type
属性值必须是file
。
确定页面中表单的action
属性的值为upload.do
。
检查spring.xml
中配置的组件扫描为cn.tedu.spring
。
创建cn.tedu.spring.UploadController
控制器类,并在类之前添加@Controller
注解,然后,添加处理请求的方法:
@Controller
public class UploadController {
@RequestMapping("upload.do")
@ResponseBody
public String upload() {
System.out.println("UploadController.upload()");
return "OK";
}
}
在SpringMVC框架中,使用了CommonsMultipartResolver
对上传的数据进行处理,需要在spring.xml中配置该类:
在配置时,必须指定id
,且值必须是multipartResolver
。
关于以上节点,可以暂不配置详细信息。
然后,在处理请求的方法中,添加MultipartFile file
参数,该参数就是用户上传的文件的数据,并在参数之前添加@RequestParam
注解。
最后,调用参数对象的void transferTo(File dest)
方法即可将用户上传的文件保存到服务器的硬盘中!
@RequestMapping("upload.do")
@ResponseBody
public String upload(
@RequestParam("file") MultipartFile file)
throws IllegalStateException, IOException {
System.out.println("UploadController.upload()");
// 执行保存文件
File dest = new File("F:/1/a.jpg");
file.transferTo(dest);
return "OK";
}
........
@RequestMapping("upload.do")
@ResponseBody
public String upload(@RequestParam("file") MultipartFile file,
HttpServletRequest request)
throws IllegalStateException, IOException {
System.out.println("UploadController.upload()");
// 判断上传的文件是否为空
boolean isEmpty = file.isEmpty();
System.out.println("\tisEmpty=" + isEmpty);
if (isEmpty) {
throw new RuntimeException("上传失败!上传的文件为空!");
}
// 检查文件大小
long fileSize = file.getSize();
System.out.println("\tsize=" + fileSize);
if (fileSize > 1 * 1024 * 1024) {
throw new RuntimeException("上传失败!上传的文件大小超出了限制!");
}
// 检查文件MIME类型
String contentType = file.getContentType();
System.out.println("\tcontentType=" + contentType);
List<String> types = new ArrayList<String>();
types.add("image/jpeg");
types.add("image/png");
types.add("image/gif");
if (!types.contains(contentType)) {
throw new RuntimeException("上传失败!不允许上传此类型的文件!");
}
// 准备文件夹
String parentDir = request.getServletContext().getRealPath("upload");
// request.getSession().getServletContext().getRealPath("");
// request.getRealPath("");
System.out.println("\tpath=" + parentDir);
File parent = new File(parentDir);
if (!parent.exists()) {
parent.mkdirs();
}
// 获取原始文件名
String originalFilename = file.getOriginalFilename();
System.out.println("\toriginalFilename=" + originalFilename);
// 确定最终保存时使用的文件
String filename = UUID.randomUUID().toString();
String suffix = "";
int beginIndex = originalFilename.lastIndexOf(".");
if (beginIndex != -1) {
suffix = originalFilename.substring(beginIndex);
}
// 执行保存文件
File dest = new File(parent, filename + suffix);
file.transferTo(dest);
return "OK";
}
为了避免文件被覆盖,每个文件的路径或文件名应该不发生冲突,例如可以使用唯一的文件名,要保证文件名唯一,可以考虑使用时间、随机数等数据作为文件名的一部分,也可以使用用户的唯一数据,例如用户的id、用户名等作为文件名的一部分,具体规则可自行决定。
关于文件的扩展名部分,可以通过MultipartFile
的String getOriginalFilename()
方法获取原始文件名,即用户上传的文件在客户端时使用的文件名,然后对文件名进行分析处理,得到原始的扩展名,例如:
String originalFilename = file.getOriginalFilename();
String suffix = "";
int beginIndex = originalFilename.lastIndexOf(".");
if (beginIndex != -1) {
suffix = originalFilename.substring(beginIndex);
}
上传的文件必须在Tomcat部署项目的目录中,否则,上传的文件将无法通过http协议进行访问,可以HttpServletRequest
对象获取在webapp
下指定名称的文件夹的实际路径,然后创建出所需的文件夹,例如,先在处理请求的方法中添加HttpServletRequest
作为参数,然后:
String parentDir = request.getServletContext().getRealPath("upload");
File parent = new File(parentDir);
if (!parent.exists()) {
parent.mkdirs();
}
String getOriginalFilename()
:获取原始文件名,即用户上传的文件在客户端时使用的文件名;
boolean isEmpty()
:判断上传的文件是否为空,如果用户没有选择文件就提交了上传请求,或选择的文件是0字节的,则视为空,将返回true
,否则返回false
;
long getSize()
:获取文件的大小,以字节为单位;
String getContentType()
:获取文件的MIME类型,例如返回image/jpeg
,关于扩展名与MIME的对应关系,可以上网查阅资料,也可以在Tomcat的conf目录下的web.xml中查找,文件的扩展名不同,得到的MIME类型可能是不同的;
InputStream getInputStream()
:获取文件的输入字节流,用于需要自定义处理用户上传的数据的应用场景,例如上传的文件较大,在存储时需要自定义缓冲区等,该方法不可与transferTo()
方法同时使用;
void transferTo(File dest)
:保存客户端上传的文件,该方法不可以与getInputStream()
方法同时使用。
在spring.xml中可以对CommonsMultipartResolver
的属性注入值:
maxUploadSize
:最大上传的数据量,以字节为单位,假设设置为10M,则单次请求的最大数据就是10M,可能这次上传过程中有2个文件,则2个文件的总和不允许超过10M;
maxUploadSizePerFile
:上传的每个文件的最大大小,假设设置为10M,且这次上传过程中有2个文件,则每个文件都不可以超过10M,但是这次请求的数据总大小可能接近20M;
defaultEncoding
:默认字符集,用于设置上传时同一个表单中可能提交的其它字符的编码。
以上设置请求数据大小的配置,检查的时间节点比较靠前,在控制器还没有处理请求时就会检查;这些设置并不能取代在控制器通过
getSize()
获取文件大小并进行的相关检查,在配置文件中的这些配置是项目的全局化配置,即同一个项目中,无论是上传头像,还是上传附件,还是上传商品图片等,所有的上传功能都必须符合这些配置,而控制器中的检查是单项功能的检查,毕竟每个控制器只处理某1种请求,所以,上传头像的方法里自定义头像的大小,而上传商品图片的方法里自定义商品图片的大小,是根据具体功能作出的限制。
如果需要上传的文件的数量是固定的,且文件的定位是不同的,例如上传身份证的正面和反面照片,则在客户端可以使用多个控件,且在服务器处理请求的方法中添加多个
MultipartFile
类型的参数即可。
另外,如果上传的文件的定位是相同的,且数量不固定,则在客户端使用控件,添加multiple="multiple
属性,即:
这样的文件浏览控件在操作时,可以按住Ctrl键,是可以一次性选中多个文件的。
然后,在服务器端的控制器中,将参数声明为MultipartFile[]
即可。
所有的上传,都应该将文件存储到服务器的硬盘中,另外,在数据库中记录下文件的存储路径,当需要使用文件时,可以查询数据库获取指定文件的路径,再对文件进行访问。
(a) 规划SQL语句
执行上传头像的数据库操作部分,只是修改用户数据的avatar
字段的值,需要执行的SQL语句大致是:
update t_user set avatar=?,modified_user=?,modified_time=? where uid=?
在执行修改之前,还应该检查用户数据是否存在、是否标记为已删除,对应的功能已经存在,无需再次开发。
(b) 接口与抽象方法
在UserMapper.java
接口中添加抽象方法:
Integer updateAvatar(
@Param("uid") Integer uid,
@Param("avatar") String avatar,
@Param("modifiedUser") String modifiedUser,
@Param("modifiedTime") Date modifiedTime);
© 配置映射
配置映射:
<update id="updateAvatar">
UPDATE
t_user
SET
avatar=#{avatar},
modified_user=#{modifiedUser},
modified_time=#{modifiedTime}
WHERE
uid=#{uid}
update>
单元测试:
@Test
public void updateAvatar() {
Integer uid = 7;
String avatar ="1234";
String modifiedUser = "超级管理员";
Date modifiedTime = new Date();
Integer rows = mapper.updateAvatar(uid, avatar, modifiedUser, modifiedTime);
System.err.println("rows=" + rows);
}
(a) 规划异常
本次执行的主要是更新数据的操作,则可能出现UpdateException
。
在执行更新之前,还需要根据uid查询用户数据,检查数据是否存在,检查数据的is_delete,都可能出现UserNotFoundException
。
此次操作无需创建新的异常类。
(b) 接口与抽象方法
在IUserService
中添加“修改头像”的抽象方法:
void changeAvatar(Integer uid, String avatar, String modifiedUser) throws UserNotFoundException, UpdateException;
© 实现抽象方法
在UserServiceImpl
实现类中添加新的抽象方法并实现:
public void changeAvatar(Integer uid, String avatar, String modifiedUser) throws UserNotFoundException, UpdateException {
// 根据参数uid查询用户数据
// 判断查询结果是否为null:UserNotFoundException
// 判断查询结果中的isDelete是否为1:UserNotFoundException
// 执行更新,获取返回值(受影响的行数)
// 判断受影响的行数是否不为1:UpdateException
}
具体实现为:
@Override
public void changeAvatar(Integer uid, String avatar, String modifiedUser)
throws UserNotFoundException, UpdateException {
// 根据参数uid查询用户数据
User result = userMapper.findByUid(uid);
// 判断查询结果是否为null:UserNotFoundException
if (result == null) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException(
"修改头像失败!用户数据不存在!");
}
// 判断查询结果中的isDelete是否为1:UserNotFoundException
if (result.getIsDelete() == 1) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException(
"修改头像失败!用户数据不存在!");
}
// 执行更新,获取返回值(受影响的行数)
Integer rows = userMapper.updateAvatar(uid, avatar, modifiedUser, new Date());
// 判断受影响的行数是否不为1:UpdateException
if (rows != 1) {
throw new UpdateException(
"修改头像失败!更新数据时出现未知错误!");
}
}
单元测试:
@Test
public void changeAvatar() {
try {
Integer uid = 7;
String avatar = "88888888";
String modifiedUser = "系统管理员";
service.changeAvatar(uid, avatar, modifiedUser);
System.err.println("OK");
} catch (ServiceException e) {
System.err.println(e.getClass().getName());
System.err.println(e.getMessage());
}
}
(a) 统一处理异常
在处理上传文件之前,应该对用户选择并提交的文件数据进行检查,例如文件是否为空、文件大小是否超出限制、文件类型是否超出限制,如果不符合标准,应该抛出对应的异常:
cn.tedu.store.controller.ex.FileEmptyException
cn.tedu.store.controller.ex.FileSizeException
cn.tedu.store.controller.ex.FileTypeException
这些都继承自cn.tedu.store.controller.ex.FileUploadException
,而该FileUploadException
继承自RuntimeException
。
另外,在执行保存文件时,调用的transferTo()
方法也是会抛出异常的:
try {
file.transferTo(dest);
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
当捕获到以上这些异常时,可以在捕获后抛出自定义异常FileStateException
和FileUploadIOException
,也是和以上异常在同一个包中,也都继承自FileUploadException
,然后,在BaseController
中对自定义异常进行统一处理!
(b) 设计请求
设计“上传头像”的请求方式:
请求路径:/users/change_avatar
请求参数:MultipartFile file, HttpServletRequest request
请求方式:POST
响应数据:JsonResult
© 处理请求
在UserController
中添加处理请求的方法:
@PostMapping("change_avatar")
public JsonResult changeAvatar(@RequestParam("file") MultipartFile file, HttpServletRequest request) {
}
当登录成功时,将用户的头像路径响应给客户端,且客户端将头像路径保存到Cookie中。首先,应该检查在UserMapper.xml
的findByUsername()
查询的字段列表中是否包含avatar
字段;然后,在登录的前端界面中,当响应结果表示登录成功时,通过$.cookie("avatar", obj.data.avatar, {"expires":7});
将服务器端响应的头像路径保存到客户端的Cookie中。
打开上传头像页面时,通过Cookie中保存的头像路径,显示当前登录的用户的头像:
在SpringBoot项目中,所有的上传默认都不允许超过1M,否则就会报错!
先在启动类StoreApplication
之前添加@Configuration
注解,然后,在类中添加:
@Bean
public MultipartConfigElement getMultipartConfigElement() {
MultipartConfigFactory factory
= new MultipartConfigFactory();
DataSize maxFileSize = DataSize.ofMegabytes(500);
factory.setMaxFileSize(maxFileSize);
DataSize maxRequestSize = DataSize.ofMegabytes(500);
factory.setMaxRequestSize(maxRequestSize);
return factory.createMultipartConfig();
}