在设计一款软件时,在编写代码之前,应该先分析这个项目中需要处理哪些类型的数据!例如,本项目中需要处理的数据种类有:收藏,购物车,用户,收货地址,订单,商品,商品类别。
当确定了需要处理的数据的种类之后,就应该确定这些数据的处理先后顺序:用户 > 收货地址 > 商品类别 > 商品 > 收藏 > 购物车 > 订单。
在具体开发某个数据的管理功能之前,还应该分析该数据需要开发哪些管理功能,以用户数据为例,需要开发的有:修改密码,上传头像,修改资料,登录,注册。
分析出功能之后,也需要确定这些功能的开发顺序,一般先开发简单的,也依据增、查、删、改的顺序,则以上功能的开发顺序应该是:注册 > 登录 > 修改密码 > 修改资料 > 上传头像。
在开发某个数据的任何功能之前,还应该先创建这种数据对应的数据表,然后,创建对应的实体类,再开发某个功能!
在开发某个功能时,还应该遵循顺序:持久层(数据库编程) > 业务层 > 控制器层 > 前端页面。
先创建数据库:
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;
进行查看。
创建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()方法
}
持久层:持久化保存数据的层。
刚创建好的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映射:
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}
)
在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("[email protected]");
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);
}
}
业务,在普通用户眼里就是“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("[email protected]");
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.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();
}
}
/**
* 执行密码加密,获取加密后的结果
* @param password 原始密码
* @param salt 盐值
* @return 加密后的结果
*/
private String getMd5Password(String password, String salt) {
// 加密标准:使用salt+password+salt作为被运算数据,循环加密3次
String result = salt + password + salt;
for (int i = 0; i < 3; i++) {
result = DigestUtils.md5Hex(result);
}
System.err.println("\tpassword=" + password);
System.err.println("\tsalt=" + salt);
System.err.println("\tmd5Password=" + result);
return result;
}
}
控制器层主要解决的问题是:接收客户端提交的请求,调用Service组件进行数据处理,并将处理结果响应给客户端。
先在src/main/java的cn.demo.store
包中创建子级util
包,并在util
中创建JsonResult
类,用于封装响应给客户端的JSON数据中的属性:
/**
* 封装响应JSON对象的属性的类
*
* @param 响应给客户端的数据的类型(泛型)
*/
public class JsonResult {
// 响应的标识,例如:使用200表示登录成功,使用400表示由于用户名不存在导致的登录失败
private Integer state;
// 操作失败/操作出错时的描述文字,例如:“登录失败,用户名不存在”
private String message;
// 操作成功时需要响应给客户端的数据
private E data;
public JsonResult() {
super();
}
public JsonResult(Integer state) {
super();
this.state = state;
}
public JsonResult(Throwable e) {
super();
this.message = e.getMessage();
}
// 自行补充SET/GET方法
}
在处理请求之前,就可以直接对相关的异常进行处理(SpringMVC统一处理):
@RestControllerAdvice
public class GlobalHandleException {
@ExceptionHandler(ServiceException.class)
public JsonResult handleException(Throwable e) {
JsonResult result = new JsonResult<>(e);
if (e instanceof UsernameDuplicateException) {
result.setState(4000);
} else if (e instanceof InsertException) {
result.setState(5000);
}
return result;
}
}
在src/main/java的cn.demo.store
包中创建子级controller
包,并在controller
包中创建UserController
类,专门用于处理用户数据相关的请求,需要在类的声明之前添加@RestController
注解,推荐在类的声明之前添加@RequestMapping("users")
注解,由于需要调用Service组件处理数据,在类中还应该声明@Autowired private UserService userService;
对象:
@RequestMapping("users")
@RestController
public class UserController {
@Autowired
private UserService userService;
}
然后,在类中添加处理请求的方法,由于异常已经被统一处理了,所以,在处理请求时,只需要以“注册成功”为前提来处理即可,不需要关心出现异常的问题:
@RequestMapping("users")
@RestController
public class UserController {
@Autowired
private UserService userService;
/**
* 响应到客户端的、表示操作成功的状态值
*/
private static final int OK = 2000;
// http://localhost:8080/users/reg
@PostMapping("reg")
public JsonResult reg(User user) {
// 调用业务对象执行注册
userService.reg(user);
// 返回成功
return new JsonResult<>(OK);
}
}
完成后,启动项目,在浏览器中打开http://localhost:8080/users/reg?username=controller&password=1234
即可测试。
登录操作,应该是先根据用户名查询用户数据,并对查询的数据进行基本有效性的判断,后续,再验证密码,如果密码也正确,就登录成功,并返回该用户的相关信息,例如uid、username、avatar等。
在数据库的操作中,需要实现的是:根据用户名查询用户数据。该功能已经实现,则不需要再次开发。
首先,在UserService
业务层接口中添加抽象方法:
User login(String username, String password);
然后,在UserServiceImpl
中实现以上方法:
@Override
public User login(String username, String password) {
// 日志
System.err.println("UserServiceImpl.login()");
// 基于参数username调用userMapper.findByUsername()查询用户数据
User result = userMapper.findByUsername(username);
// 判断查询结果(result)是否为null
if (result == null) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException("登录失败,用户名不存在!");
}
// 判断查询结果(result)中的isDelete是否为1
if (result.getIsDelete() == 1) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException("登录失败,用户数据已经被删除!");
}
// 从查询结果(result)中获取盐值
String salt = result.getSalt();
// 基于参数password和盐值,调用getMd5Password()执行加密
String md5Password = getMd5Password(password, salt);
// 判断查询结果(result)中的密码和以上加密结果是否不一致
if (!md5Password.equals(result.getPassword())) {
// 是:抛出PasswordNotMatchException
throw new PasswordNotMatchException("登录失败,密码错误!");
}
// 创建新的User对象
User user = new User();
// 将查询结果中的uid、username、avatar设置到新的User对象的对应的属性中
user.setUid(result.getUid());
user.setUsername(result.getUsername());
user.setAvatar(result.getAvatar());
// 返回新创建的User对象
return user;
}
最后,在UserServiceTests
中编写并执行单元测试:
@Test
public void login() {
try {
String username = "digests";
String password = "0000";
User result = service.login(username, password);
System.err.println("OK.");
System.err.println(result);
} catch (ServiceException e) {
System.err.println(e.getClass().getName());
System.err.println(e.getMessage());
}
}
由于使用了统一处理异常的做法,在这种做法中,异常的处理方式也非常简单,所以,应该优先把新的异常都处理掉,在GlobalExceptionHandler
中添加更多的else if
进行判断并处理即可。
为了便于向客户端响应数据,在JsonResult
中添加新的构造方法:
public JsonResult(Integer state, E data) {
super();
this.state = state;
this.data = data;
}
然后,在UserController
中添加处理“登录”的方法:
// http://localhost:8080/users/login?username=digest&password=0000
@PostMapping("login")
public JsonResult login(String username, String password, HttpSession session) {
// 调用userService.login()方法执行登录,并获取返回结果(成功登录的用户数据)
User data = userService.login(username, password);
// 将返回结果中的uid和username存入到Session
session.setAttribute("uid", data.getUid());
session.setAttribute("username", data.getUsername());
// 将结果响应给客户端
return new JsonResult<>(OK, data);
}
完成后,启动整个项目,打开浏览器,通过http://localhost:8080/users/login?username=digest&password=0000
测试登录功能是否正常!
在测试过程中,可以发现,许多为null
属性也在JSON结果中,这样会浪费流量,也会暴露数据的结构,应该将使得这些为null
的属性不出现在JSON结果中,可以在application.properties中添加配置:
spring.jackson.default-property-inclusion=NON_NULL
在UserMapper
接口中添加以下抽象方法:
/**
* 更新用户的密码
* @param uid 用户的id
* @param password 新的密码
* @param modifiedUser 最后修改人
* @param modifiedTime 最后修改时间
* @return 受影响的行数
*/
Integer updatePasswordByUid(
@Param("uid") Integer uid,
@Param("password") String password,
@Param("modifiedUser") String modifiedUser,
@Param("modifiedTime") Date modifiedTime
);
/**
* 根据用户id查询用户数据
* @param uid 用户id
* @return 匹配的用户数据,如果没有匹配的数据,则返回null
*/
User findByUid(Integer uid);
然后,在UserMapper.xml中配置以上2个方法对应的SQL语句:
UPDATE
t_user
SET
password=#{password},
modified_user=#{modifiedUser},
modified_time=#{modifiedTime}
WHERE
uid=#{uid}
最后,在UserMapperTests中编写并执行单元测试:
@Test
public void updatePasswordByUid() {
Integer uid = 1;
String password = "888888";
String modifiedUser = "系统管理员";
Date modifiedTime = new Date();
Integer rows = mapper.updatePasswordByUid(uid, password, modifiedUser, modifiedTime);
System.err.println("rows=" + rows);
}
@Test
public void findByUid() {
Integer uid = 1;
User result = mapper.findByUid(uid);
System.err.println(result);
}
由于处理业务过程中,可能会抛出异常,所以,需要先创建相关的异常类!
此次需要创建的是UpdateException
异常类:
package cn.demo.store.servic.ex;
// 自行添加类的注释
public class UpdateException extends ServiceException {
// 自行添加序列化id
// 自行添加5个构造方法
}
在业务层接口UserService
中添加抽象方法:
/**
* 修改密码
* @param uid 用户的id
* @param username 用户名
* @param oldPassword 原密码
* @param newPassword 新密码
*/
void changePassword(Integer uid, String username, String oldPassword, String newPassword);
然后,在UserServiceImpl
中实现以上方法:
@Override
public void changePassword(Integer uid, String username, String oldPassword, String newPassword) {
System.err.println("UserServiceImpl.changePassword()");
// 调用userMapper.findByUid()查询用户数据
User result = userMapper.findByUid(uid);
// 判断查询结果(result)是否为null
if (result == null) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException("修改密码失败,尝试访问的用户数据不存在!");
}
// 判断查询结果(result)中的isDelete属性是否为1
if (result.getIsDelete() == 1) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException("修改密码失败,用户数据已被删除!");
}
// 从查询结果(result)中取出盐值(salt)
String salt = result.getSalt();
// 基于参数oldPassword和盐值执行加密
String oldMd5Password = getMd5Password(oldPassword, salt);
// 判断以上加密结果与查询结果(result)中的密码是否不匹配
if (!oldMd5Password.equals(result.getPassword())) {
// 是:抛出PasswordNotMatchException
throw new PasswordNotMatchException("修改密码失败,原密码错误!");
}
// 日志
System.err.println("\t验证通过,更新密码:");
// 基于参数newPassword和盐值执行加密
String newMd5Password = getMd5Password(newPassword, salt);
// 调用userMapper.updatePasswordByUid()执行更新密码(最后修改人是参数username),并获取返回值
Integer rows = userMapper.updatePasswordByUid(uid, newMd5Password, username, new Date());
// 判断返回结果是否不为1
if (rows != 1) {
// 是:抛出UpdateException
throw new UpdateException("修改密码失败,更新密码时出现未知错误,请联系系统管理员!");
}
}
最后,在UserServiceTests
中编写并执行单元测试:
@Test
public void changePassword() {
try {
Integer uid = 5;
String username = "密码管理员";
String oldPassword = "1234";
String newPassword = "0000";
service.changePassword(uid, username, oldPassword, newPassword);
System.err.println("OK.");
} catch (ServiceException e) {
System.err.println(e.getClass().getName());
System.err.println(e.getMessage());
}
}
在UserController
中添加处理“修改密码”请求的方法:
@PostMapping("password/change")
public JsonResult changePassword(String oldPassword, String newPassword, HttpSession session) {
// 从Session中取出uid和username
Integer uid = Integer.valueOf(session.getAttribute("uid").toString());
String username = session.getAttribute("username").toString();
// 调用userService.changePassword()执行修改密码
userService.changePassword(uid, username, oldPassword, newPassword);
// 返回操作成功
return new JsonResult<>(OK);
}
因为后续将有很多操作都是必须登录才允许访问的,如果在每个处理请求的方法中判断,工作量较大,且不利于统一管理,所以,应该通过拦截器来处理!
在SpringBoot项目中,拦截器类的写法与普通的SpringMVC项目中是相同的!所以,先在cn.demo.store
包下创建子级的interceptor
包,然后在interceptor
包下创建LoginInterceptor
,需要实现HandlerInterceptor
接口,并重写preHandle()
方法:
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (request.getSession().getAttribute("uid") == null) {
response.sendRedirect("/web/login.html");
return false;
}
return true;
}
}
然后,还需要配置拦截器,在SpringBoot项目中,关于拦截器的配置,需要自定义配置类:
先在cn.demo.store
包下创建子级的config
配置包,然后在config
包下创建InterceptorConfiguration类并实现WebMvcConfigurer,添加注解**@Configuration**
/**
* 拦截器的配置类
*/
@Configuration
public class InterceptorConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
HandlerInterceptor interceptor = new LoginInterceptor();
List 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);
}
}
关于修改个人资料,需要解决的问题有2个:
执行修改个人资料之前,需要显示当前登录的用户的个人资料,就需要事先获取当前登录的用户的个人资料,对应的SQL语句大致是:
select * from t_user where uid=?
以上查询功能已经开发,则无需重复开发。
执行修改个人资料时,需要执行的是更新数据的操作,对应的SQL语句大致是:
update t_user set phone=?, email=?, gender=?, modified_user=?, modified_time=? where uid=?
在UserMapper
接口中添加抽象方法:
/**
* 更新用户的个人资料
* @param user 封装了用户的id和新个人资料的对象,可以更新的属性有:手机号码,电子邮箱,性别
* @return 受影响的行数
*/
Integer updateInfoByUid(User user);
在UserMapper.xml中配置以上抽象方法的映射:
UPDATE
t_user
SET
gender=#{gender},
phone=#{phone},
email=#{email},
modified_user=#{modifiedUser},
modified_time=#{modifiedTime}
WHERE
uid=#{uid}
在UserMapperTests
中编写并测试以上方法:
@Test
public void updateInfoByUid() {
User user = new User();
user.setUid(5);
user.setPhone("13000130000");
user.setEmail("[email protected]");
user.setGender(0);
Integer rows = mapper.updateInfoByUid(user);
System.err.println("rows=" + rows);
}
当需要显示个人资料时:直接查询用户的数据,进行相关的检查,完成后,就可以将数据返回了,在整个过程中,涉及的异常可能有:UserNotFoundException
;
当需要修改个人资料时:应该先查询用户的数据,对查询结果进行相关检查,检查无误后,则执行更新,在整个过程中,涉及的异常可能有:UserNotFoundException
,UpdateException
。
在UserService
接口中添加抽象方法:
/**
* 获取用户个人资料数据
* @param uid 用户id
* @return 返回封装了用户个人资料的User
*/
User getInfo(Integer uid);
/**
* 修改用户资料
* @param uid 用户id
* @param username 用户名
* @param user 要修改的个人资料数据
*/
void changeInfo(Integer uid, String username, User user);
在UserServiceImpl
类中实现以上抽象方法:
@Override
public User getInfo(Integer uid) {
// 调用userMapper.findByUid()查询用户数据
User result = userMapper.findByUid(uid);
// 判断查询结果(result)是否为null
if (result == null) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException("获取用户数据失败,尝试访问的用户数据不存在!");
}
// 判断查询结果(result)中的isDelete属性是否为1
if (result.getIsDelete() == 1) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException("获取用户数据失败,用户数据已被删除!");
}
// 创建新的User对象
User user = new User();
// 通过查询结果向新User对象中封装属性:username,phone,email,gender
user.setUsername(result.getUsername());
user.setPhone(result.getPhone());
user.setEmail(result.getEmail());
user.setGender(result.getGender());
// 返回新User对象
return user;
}
@Override
public void changeInfo(Integer uid, String username, User user) {
// 调用userMapper.findByUid()查询用户数据
User result = userMapper.findByUid(uid);
// 判断查询结果(result)是否为null
if (result == null) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException("修改用户资料失败,尝试访问的用户数据不存在!");
}
// 判断查询结果(result)中的isDelete属性是否为1
if (result.getIsDelete() == 1) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException("修改用户资料失败,用户数据已被删除!");
}
// 向参数user中补充数据:uid > 参数uid
user.setUid(uid);
// 向参数user中补充数据:modifiedUser > 参数username
user.setModifiedUser(username);
// 向参数user中补充数据:modifiedTime > new Date()
user.setModifiedTime(new Date());
// 调用userMapper.updateInfoByUid()执行更新,并获取返回值
Integer rows = userMapper.updateInfoByUid(user);
// 判断返回值是否不为1
if (rows != 1) {
// 是:抛出UpdateException
throw new UpdateException("修改用户资料失败,更新用户资料时出现未知错误,请联系系统管理员!");
}
}
完成后,在UserServiceTests
中编写并执行单元测试:
@Test
public void getInfo() {
try {
Integer uid = 5;
User result = service.getInfo(uid);
System.err.println("OK.");
System.err.println(result);
} catch (ServiceException e) {
System.err.println(e.getClass().getName());
System.err.println(e.getMessage());
}
}
@Test
public void changeInfo() {
try {
Integer uid = 5;
String username = "资料管理员";
User user = new User();
user.setPhone("13804380438");
user.setEmail("[email protected]");
user.setGender(1);
service.changeInfo(uid, username, user);
System.err.println("OK.");
} catch (ServiceException e) {
System.err.println(e.getClass().getName());
System.err.println(e.getMessage());
}
}
关于显示个人资料
/users/info/show
HttpSession session
(严格来说,并不需要客户端提交参数,应该是从Session中获取uid即可)GET
JsonResult
关于修改个人资料
/users/info/change
User user
HttpSession session
(严格来说,需要的是Session中的uid和username)POST
JsonResult
在UserController
中添加处理请求的方法:
// http://localhost:8080/users/info/show
@GetMapping("info/show")
public JsonResult showInfo(HttpSession session) {
// 从Session中获取uid
Integer uid = Integer.valueOf(session.getAttribute("uid").toString());
// 调用userService.getInfo()获取数据
User data = userService.getInfo(uid);
// 响应成功及数据
return new JsonResult<>(OK, data);
}
@PostMapping("info/change")
public JsonResult changeInfo(User user, HttpSession session) {
// 从Session中获取uid和username
Integer uid = Integer.valueOf(session.getAttribute("uid").toString());
String username = session.getAttribute("username").toString();
// 调用userService.changeInfo()修改个人资料
userService.changeInfo(uid, username, user);
// 响应成功
return new JsonResult<>(OK);
}
上传头像的本质是:将客户端选中并提交的文件保存到webapp下(也可以是SpringBoot项目的src/main/resources/static下),并且,在数据库中记录下该文件的路径(包含文件名),后续,当需要访问该头像时,从数据库中读取此前保存的路径,通过该路径就可以访问到头像文件。所以,上传头像时,需要执行的操作有2个:将文件保存下来,将路径记录到数据库中。
保存客户端选中并上传的文件,应该在控制器层进行处理,一般,上传技术都是由控制器技术提供的,例如,在传统的Java EE环境中,就有基于Servlet的文件上传,在处理控制器时,可以使用Struts2框架或SpringMVC框架,这些框架也都提供更加简便的文件上传的处理方式,所以,文件上传的“保存文件”操作是与控制器密切相关的,就应该由控制器层进行处理!
所以,在持久层需要处理的就只有更新数据表中用户头像字段的值!需要执行的SQL语句大致是:
update t_user set avatar=?, modifedUser=?, modifiedTime=? where uid=?
在执行更新之前,还应该检查数据的有效性(用户数据是否存在,是否被标记为删除)。
在UserMapper
接口中添加抽象方法:
/**
* 更新用户头像
* @param uid 用户Id
* @param avatar 头像路径
* @param modifiedUser 最后修改人
* @param modifiedTime 修改时间
* @return
*/
Integer updateAvatarByUid(
@Param("uid") Integer uid,
@Param("avatar") String avatar,
@Param("modifiedUser") String modifiedUser,
@Param("modifiedTime") Date modifiedTime
);
在UserMapper.xml中配置以上抽象方法对应的SQL语句:
UPDATE
t_user
SET
avatar=#{avatar},
modified_user=#{modifiedUser},
modified_time=#{modifiedTime}
WHERE
uid=#{uid}
最后,在UserMapperTests
中编写并执行单元测试:
@Test
public void updateAvatarByUid() {
Integer uid = 6;
String avatar = "头像路径";
String modifiedUser = "头像管理员";
Date modifiedTime = new Date();
Integer rows = mapper.updateAvatarByUid(uid, avatar, modifiedUser, modifiedTime);
System.err.println("rows=" + rows);
}
在业务层处理上传头像时,依然是先检查用户数据的有效性,检查完成后,允许执行更新头像。
在UserService
接口中添加抽象方法:
void changeAvatar(Integer uid, String username, String avatar);
在UserServiceImpl
类中实现以上抽象方法:
具体代码为:
@Override
public void changeAvatar(Integer uid, String username, String avatar) {
// 调用userMapper.findByUid()查询用户数据
User result = userMapper.findByUid(uid);
// 判断查询结果(result)是否为null
if (result == null) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException("修改用户头像失败,尝试访问的用户数据不存在!");
}
// 判断查询结果(result)中的isDelete属性是否为1
if (result.getIsDelete() == 1) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException("修改用户头像失败,用户数据已被删除!");
}
// 调用userMapper.updateAvatarByUid()执行更新,并获取返回值
Integer rows = userMapper.updateAvatarByUid(uid, avatar, username, new Date());
// 判断返回值是否不为1
if (rows != 1) {
// 是:抛出UpdateException
throw new UpdateException("修改用户头像失败,更新头像时出现未知错误,请联系系统管理员!");
}
}
最后,在UserServiceTests
中编写并执行单元测试:
@Test
public void changeAvatar() {
try {
Integer uid = 5;
String username = "管理员";
String avatar = "1234";
service.changeAvatar(uid, username, avatar);
System.err.println("OK.");
} catch (ServiceException e) {
System.err.println(e.getClass().getName());
System.err.println(e.getMessage());
}
}
在处理上传时,主要使用MultipartFile
表示客户端上传的文件,这种MutlipartFile
是服务器端对客户端上传的文件数据进行封装了的对象,它不仅仅只是文件数据而已,还封装了与文件数据相关的其它数据,可以通过相关API获取这些数据,常用的MultipartFile
的API有:
String getOriginalFilename()
:获取上传的文件的原始名称,即这个文件在客户端设备中的名称;boolean isEmpty()
:判断上传的文件是否为空,如果在上传的表单中没有选择文件,或选择的文件是0字节的,则返回true
,否则,返回false
;long getSize()
:获取客户端上传的文件的大小,以字节为单位;String getContentType()
:获取客户端上传的文件的MIME类型,是根据文件的扩展名得到的;void transferTo(File dest)
:执行保存客户端上传的文件,参数就是保存到的位置。在执行上传时,如果上传的文件为空,或上传的文件大小超标,或上传的文件类型不符,都应该抛出对应的异常,这些异常都是在控制器中处理上传时出现的,并不是处理业务过程中出现的,所以,不应该继承自原有的ServiceException
,应该为这些异常创建新的FileUploadException
基类,该基类是继承自RuntimeException
的,而对应某种具体错误的异常都应该继承自这个基类异常!
另外,在保存上传的文件时(调用MultipartFile
对象的transferTo()
方法),会抛出2种异常,在具体处理时,也应该捕获这2种异常,并在捕获后抛出对应的FileStateException
和FileUploadIOException
,这2个自定义异常也应该继承自FileUploadException
。
所以,应该先创建相关的异常类,它们应该是:
package cn.demo.store.controller.ex;
public class FileUploadException extends RuntimeException {
// 生成序列化版本id
// 生成5个构造方法
}
----------------------------------------------------
public class FileEmptyException extends FileUploadException {
// 生成序列化版本id
// 生成5个构造方法
}
----------------------------------------------------
public class FileSizeException extends FileUploadException {
// 生成序列化版本id
// 生成5个构造方法
}
----------------------------------------------------
public class FileTypeException extends FileUploadException {
// 生成序列化版本id
// 生成5个构造方法
}
----------------------------------------------------
public class FileStateException extends FileUploadException {
// 生成序列化版本id
// 生成5个构造方法
}
----------------------------------------------------
public class FileUploadIOException extends FileUploadException {
// 生成序列化版本id
// 生成5个构造方法
}
当创建了以上异常类之后,应该在GlobalHandleException
的统一处理异常的过程中,对以上异常进行处理!
首先,原有处理异常的方法添加了@ExceptionHandler(ServiceException.class)
,则表示该方法只处理ServiceException
及其子孙类异常,而以上创建的异常类与ServiceException
并没有继承关系,将不会被处理,所以,需要先修改注解参数:
@ExceptionHandler({ServiceException.class, FileUploadException.class})
则以上注解对应的方法可以处理ServiceException
和FileUploadException
这2大类异常!
然后,在处理过程中,添加else if
分支进行判断并处理即可!
在application.properties中配置上传的文件大小、文件类型的限制:
project.avatar-max-size=112640 # 自定义名称
project.avatar-types=image/png,image/jpeg,image/gif
如果是基本值(数值、字符串、布尔值),在配置属性时,等于号的右侧直接写值就可以,如果是List
类型的,则各个值之间使用逗号分隔,如果是数组类型的,则使用相同的属性名加上[0]
类似的下标,配置多行属性。
后续,在程序中,在全局属性之前通过@Value(${属性名})
即可读取以上的配置值。
在控制器类中,声明全局属性,在属性的声明之前添加@Value
注解,以读取以上自定义配置:
/**
* 上传头像时,允许使用的文件的最大大小,使用字节为单位
*/
@Value("${project.avatar-max-size}")
private int avatarMaxSize;
/**
* 上传头像时,允许使用的头像文件的MIME类型
*/
@Value("${project.avatar-types}")
private List avatarTypes;
在控制器类中,添加处理请求的方法:
/**
* 上传头像文件时的最大大小,使用字节为单位(从配置文件读取)
*/
@Value("${project.avatar-max-size}")
private int avatarMaxSize;
/**
* 上传头像时允许的图片类型(从配置文件中读取)
*/
@Value("${project.avatar-types}")
private List<String> avatarTypes;
@PostMapping("avatar/change")
public JsonResult<String> changeAvatar(MultipartFile file,HttpSession session){
System.err.println("UserController.changeAvatar()");
// 判断上传文件是否为空
boolean isEmpty = file.isEmpty();
if(isEmpty) {
throw new FileEmptyException("上传文件失败!请选择有效的头像文件!");
}
// 上传文件的大小(SpringBoot框架默认限制了上传文件的大小)
long size = file.getSize();
if(size > avatarMaxSize) {
throw new FileSizeException("上传文件失败,不允许上传超过"+(avatarMaxSize/1024)+"KB大小的图片文件");
}
// 上传文件的类型
String contentType = file.getContentType();
if(!avatarTypes.contains(contentType)) {
throw new FileTypeException("上传头像失败,只允许上传如下格式:\n\n"+ avatarTypes);
}
// 创建保存头像文件的目录(需要的话也可以写在properties配置文件中)
String dirName = "upload";
// 获取webapp下的某个文件夹的真实路径,"upload"是要创建的子目录名称
String parentPath = session.getServletContext().getRealPath(dirName);
// 目标文件夹
File parent = new File(parentPath);
if(!parent.exists()) {
parent.mkdirs();
}
// 上传的文件保存的文件名(用当前时间表示,防止重复)
String filename = ""+System.currentTimeMillis()+System.nanoTime();
// 上传的文件的原始名
String originalFilename = file.getOriginalFilename();
// 上传的文件保存的后缀名
/* 如果原文件名中没有小数点,则返回-1,在这种情况下,还调用substring截取,就会出现StringIndexOutOfBoundsException
如果原文件名中只有1个小数点,且是文件名的第1个字符,这样的命名方式其实是表示Linux系统中的隐藏文件,且substring是不合理的
可能需要进行 if (beginIndex > 0) 的判断
(以上判断因为在上面对上传文件的类型做了处理,所以得到的都是正确的文件格式,以上判断就不需要了)
*/
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
// 文件名称
String child = filename + suffix;
// 上传的文件保存的路径及名字
File dest = new File(parent, child);
// 执行保存文件
try {
file.transferTo(dest );
} catch (IllegalStateException e) {
throw new FileStateException("上传文件失败!原文件可能被删除, 请稍后尝试!");
} catch (IOException e) {
throw new FileUploadIOException("上传文件失败!原文件读写出错,请稍后尝试");
}
// 将上传的文件路径保存到数据库中
Integer uid = Integer.valueOf(session.getAttribute("uid").toString());
String username = session.getAttribute("username").toString();
String avatar = "/"+ dirName +"/" + child;
userService.changeAvatar(uid, username, avatar );
// 响应成功与头像路径
return new JsonResult<>(OK, avatar);
}
在启动类中(StoreApplication)添加方法进行配置:
/**
* 获取MultipartConfigElement
* 添加了@Bean注解的方法,会被spring框架调用并管理返回的类型
* @return MultipartConfigElement类型对象,是上传文件的配置类型的对象
*/
@Bean
public MultipartConfigElement getMultipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
// 关于文件上传的全局配置
// 头像500KB,买家秀图片1MB,买家秀视频5MB
// 上传的文件的最大大小
factory.setMaxFileSize(DataSize.ofMegabytes(5));
// 请求的数据量的最大大小(请求数据量包含文件大小)
factory.setMaxRequestSize(DataSize.ofMegabytes(5));
return factory.createMultipartConfig();
}
<script type="text/javascript">
// 文档加载完执行的操作
$(document).ready(function(){
var avatar = $.cookie("avatar");
if(avatar != null){
$("#img-avatar").attr("src",avatar);
}
});
$("#btn-change-avatar").click(function(){
$.ajax({
"url":"/users/avatar/change",
"data":new FormData($("#form-change-avatar")[0]),
"contentType":false,
"processData":false,
"type":"post",
"dateType":"json",
"success":function(json){
if(json.state==200){
alert("修改头像成功!"+json.date);
// 显示新头像
$("#img-avatar").attr("src",json.date);
// 把新头像路径更新到cookie中
$.cookie("avatar", json.date, {
"expires":7});
}else{
alert(json.message);
}
},
"error":function(){
alert("您的登录信息已过期,请重新登录!");
}
});
});
</script>
显示头像的逻辑应该是:
接下来,应该在登录成功之后,将得到的头像数据保存在Cookie中!
如果需要使用Cookie,可以通过jQuery中的$.cookie()
函数来实现,如果需要向Cookie中存入数据,其语法格式是:
$.cookie(名称, 值, {"expires": 有效多少天});
如果需要读取Cookie中的已经保存的数据,语法格式是:
var 值 = $.cookie(名称);
alert("登录成功!");
if(json.date.avatar == undefined){
$.cookie("avatar", null, {
"expires":7});
}else{
// 将头像路径保存进cookie
$.cookie("avatar", json.date.avatar, {
"expires":7});
}
然后,在需要显示头像的页面,例如upload.html中,需要先检查是否引用了使用jQuery中的Cookie的文件,默认在upload.html中并没有引用该文件,所以,需要先补充:
当页面刚刚加载时,就直接读取Cookie中保存的头像信息,并显示在``标签中:
$(document).ready(function() {
var avatar = $.cookie("avatar");
if (avatar != null) {
$("#img-avatar").attr("src", avatar);
}
});
最后,当上传成功后,还应该把新路径更新到Cookie中:
$.cookie("avatar", json.data, {"expires":7});
SpringMVC允许使用某个方法处理多种不同的异常,该方法的声明应该是:
public
权限;@ExceptionHandler
注解。例如:
@ExceptionHandler
public JsonResult handleException(Throwable e) {
JsonResult result = new JsonResult();
if (e instanceof UsernameDuplicateException) {
result.setState(2);
result.setMessage("【ExceptionHandler】注册失败,用户名已经被占用!");
} else if (e instanceof InsertException) {
result.setState(3);
result.setMessage("【ExceptionHandler】注册失败,保存注册数据时出现未知错误,请联系系统管理员!");
}
return result;
}
但是,并不是所有的异常都应该这样来处理,例如NullPointerException
、ClassCastException
这些都不应该这样处理,这个方法中也不可能穷举所有的异常,所以,还可以在@ExceptionHandler
注解中添加参数的配置,关于@ExceptionHandler
注解的源代码:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
/**
* Exceptions handled by the annotated method. If empty, will default to any
* exceptions listed in the method argument list.
*/
Class extends Throwable>[] value() default {};
}
则使用注解时,应该配置为:
@ExceptionHandler(ServiceException.class)
所以,在创建自定义异常时,应该给自定义的异常创建公共的父类(基类),便于统一表示这些自定义异常。
创建的自定义异常也应该是RuntimeException
的子孙类,则,调用可能抛出异常的方法时,不必强制在语法中进行try...catch
或throws
。
关于这种统一处理异常的方法,只能作用于当前控制器类中,如果处理异常的代码并不在当前类中,可以:
@ControllerAdvice
或@RestControllerAdvice
注解,这2个注解在普通的SpringMVC项目中默认是不识别的,需要自行配置,在SpringBoot项目中可以直接使用。所以,最终,统一处理异常的代码是:
@RestControllerAdvice
public class GlobalHandleException {
@ExceptionHandler(ServiceException.class)
public JsonResult handleException(Throwable e) {
JsonResult result = new JsonResult();
if (e instanceof UsernameDuplicateException) {
result.setState(2);
result.setMessage("注册失败,用户名已经被占用!");
} else if (e instanceof InsertException) {
result.setState(3);
result.setMessage("注册失败,保存注册数据时出现未知错误,请联系系统管理员!");
}
return result;
}
}