用户相关的URL
已经编写完了4
个,这次继续来编写/account/register
,也就是用户注册功能。不过首先还是看一下已经编写好的界面吧:
目前界面
登录页面:
登录失败页面:
登出页面:
虽然没什么图片和炫酷设计,不过对于一个内部工具来说足够了。
下边继续编写用户注册功能,这一套编写好之后,其实未来的项目也可以直接拿过来使用。
数据对象设计
到目前为止没有用户注册功能,你可能会问猪哥是怎么登陆成功的,当然是我手工用Bcyrpt
把密码塞到数据库里的,所以才能通过验证。编写好注册功能之后就不需要这么办了。
用户注册功能其实与其他的读写数据库功能没有什么不同,也是一个完整的CRUD
操作,在此之前,还是需要好好设计一下,先来了解一下各种数据对象。
关于各种数据对象,我在CSDN
上找了一张图:
原文里写了各种数据对象的定义,我就不一一贴过来了。
针对用户注册,这里也需要先简单的设计一下这些类:
最下边是三个PO
类,由于User
有外键连接到另外两个表,所以通过获取User
对象就可以得到已经存放在数据库内的某个用户的全部信息。目前获取用户的接口是UserAuthService
,看来可以将其名称修改成UserService
了。
然后从前端来看,目前用户提交的数据应该包含username
,password1
,password2
,email
,这个很显然需要一个VO
来验证并且存储经过验证之后的数据,就用UserRegistration
作为这个VO
。
经过验证之后的数据,如果是新用户,那么还需要一个BO
对象,这个对象应该是一个新的用户,其中应该包含一个新建的User
对象,一个新建的UserInfo
对象,以及已经确定的已经存在的List<Role>
对象,即用户相关的全部内容,最后交给UserService
来进行持久化。反过来说,UserService
也需要返回一个UserData
对象,未来在修改用户信息等场景的时候,就可以传递UserData
对象给控制器。
完善PO
类 - 编写UserInfo
类
目前已经有了User
和Role
这两个Entity
类,也就是属于这里的PO
类,即直接可以对应数据库记录的类。这两个类还需要实现Serializable
接口,并且添加一个serialVersionUID
如下:
private static final long serialVersionUID = 1L;
我将User
类的serialVersionUID
设置为1L
,Role
为2L
。
之后为了取回密码等操作,实际上我打算还需要获取用户的电子邮箱地址,甚至未来可能还需要获取用户的其他信息,但是又不想增加User
表的内容,那么这里可以再添加一个PO
类,即UserInfo
,让其与User
是强力一对一的关系,二者同生共死,以后有额外的非关键信息,都可以添加在UserInfo
表中。
所以我们还需要第三个类,就是UserInfo
类,与User
一对一的关系,目前就先在其中只添加一个字段email
:
package cc.conyli.fms.entity;
import javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import java.io.Serializable;
import java.util.Objects;
@Entity
@Table(name="userinfo")
public class UserInfo implements Serializable {
private static final long serialVersionUID = 3L;
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "id")
private Integer id;
@NotEmpty(message = "邮箱不能为空")
@Column(name = "email",nullable = false)
@Email(message = "邮箱格式不正确")
private String email;
public UserInfo() {
}
public UserInfo(String email) {
this.email = email;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserInfo userInfo = (UserInfo) o;
return id.equals(userInfo.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString() {
return "UserInfo{" +
"id=" + id +
", email='" + email + '\'' +
'}';
}
}
完善PO
类 - User
类中添加映射关系
然后在User
中,需要添加一个OneToOne
映射对应UserInfo
类。Hibernate映射一对一的方式有很多,我们这里采用比较简单的,给User
额外添加一个外键列的方式,也即User
是主表,UserInfo
是从表,二者的一对一关系主要由User
表来控制。由于我们要求每个用户至少要提供电子邮件地址,也就是说这个外键列不能为null
,而且必须unique
,以保证一对一关系。修改User
类添加一个新的属性:
@OneToOne(fetch = FetchType.LAZY,cascade = CascadeType.ALL, optional = false)
@JoinColumn(unique = true)
private UserInfo userInfo;
Hibernate
的@OneToOne
注解默认的FetchType
是EAGER
,由于不是在全部情况下都需要UserInfo
的数据,比如验证的时候就不需要,只有在修改或者重置密码的时候才需要,所以将其设置为LAZY
;由于二者是强的一对一关系,所以把cascade
设置为ALL
;optional
表示这一列不能为空。下边用@JoinColumn
注解表示这是外键列,设置unique
属性,这样就完成了外键列的设置。我的旧博客里关于Hibernate一对一映射的总结还是相当有用的。
再给User
类补上Getter
和Setter
方法,就完成了目前三个PO
类的准备工作。此时必须把数据库里User
、Role
表都清空或者干脆删除,再重新启动项目,Hibernate
就会自动创建表格。如果不清空,Hibernate
会在添加外键列的时候报错,因为添加列的时候无法满足约束条件。在重新建表之后,可以手工塞入几个数据来测试一下,我目前的设计是:Role
暂时分为ADMIN
,SUPERUSER
,USER
三种。等编写到具体功能的时候再想想看如何对应这些角色。
继续往下之前,因为我们开始区分了PO
,VO
,BO
,就重新整理一下包,目前所有的PO
位于cc.conyli.fms.entity
包下,我们创建cc.conyli.fms.dataobject
包,然后在其中创建entity
包用于存放PO,创建valueobject
存放VO
类,businessobject
存放BO
类。利用IDEA
的Refactor
功能就可以很方便的完成。
编写BO
类
在cc.conyli.fms.dataobject
包中创建businessobject
包,然后创建UserData
类,这个类主要由业务层来使用,负责把数据传递给控制器层或者将其持久化。
这个类实际上就是对User
及其相关信息的一个封装,而且是一次性将全部信息都取出来。与目前UserService.loadUserByUsername(String s)
返回的User不同,后者由于是懒加载,并不需要加载UserInfo
信息。编写如下:
package cc.conyli.fms.dataobject.businessobject;
import cc.conyli.fms.dataobject.entity.Role;
import cc.conyli.fms.dataobject.entity.User;
import cc.conyli.fms.dataobject.entity.UserInfo;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class UserData {
private final User user;
private final List<Role> roles;
private final UserInfo userInfo;
public UserData(User user, List<Role> roles, UserInfo userInfo) {
this.user = user;
this.roles = roles;
this.userInfo = userInfo;
}
public String getUsername() {
return user.getUsername();
}
public String getPassword() {
return user.getPassword();
}
public List<Role> getRoles() {
return roles;
}
public List<String> getRoleNames() {
return roles.stream().map(Role::getRole).collect(Collectors.toList());
}
public String getEmail() {
return userInfo.getEmail();
}
public User getUser() {
return user;
}
public UserInfo getUserInfo() {
return userInfo;
}
@Override
public String toString() {
return "UserData{" +
"user=" + user +
", roles=" + roles +
", userInfo=" + userInfo +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserData userData = (UserData) o;
return user.equals(userData.user);
}
@Override
public int hashCode() {
return Objects.hash(user);
}
}
这个类设计成不可变的类,要么由业务层从数据库中取出装配好,要么由控制器通过数据验证之后,交给业务层进行持久化。判断两个UserData
是否相同的标准是其中包含的User
类是否相同,而最终还是判断用户id
是否相同。
业务层添加组装UserData
的方法
目前的UserService
类有一个方法,返回实现了UserDetails
接口的User
类对象,专门用于配合Spring Security
进行验证,现在给这个业务类添加属于我们自己的方法了:
@Transactional
public UserData getUserDataByUsername(String username) throws RuntimeException {
User user = userDao.findByUsername(username);
if (user == null) {
throw new RuntimeException("用户不存在");
} else {
return new UserData(user, user.getRoles(), user.getUserInfo());
}
}
这个方法中要注意一点,不能像同一个类中的loadUserByUsername
方法一样抛出UsernameNotFoundException
异常,因为UsernameNotFoundException
是一个Spring Security
中的验证异常,出现这个异常会直接导致Spring Security
取消当前的认证状态,即使已经登录也会被取消登录。这里直接就抛出普通的RuntimeException
即可,这个错误也像之前的404
错误一样被捕捉并且被Thymeleaf
渲染到我们编写的error.html
中。
然后就可以把UserService
注入到AccountController
啦:
private final UserService userService;
@Autowired
public AccountController(UserService userService) {
this.userService = userService;
}
简单测试
在AccountController
中加入一个测试方法:
@RequestMapping("/test")
public String test() {
UserData userData = userService.getUserDataByUsername("minkolee");
System.out.println(userData.getRoleNames());
return "redirect:/";
}
访问这个路径就可以看到打印出的用户信息,如果将用户名换成不存在的用户,就可以看到如下图所示的错误页面:
编写Role
与UserInfo
的Dao
类
这两个Dao
类就是两个接口,直接继承CrudRepository
接口即可:
public interface RoleDao extends CrudRepository<Role, Integer> {
Role findByRole(String roleName);
}
另外一个UserInfoDao
不需要什么特殊方法:
public interface UserInfoDao extends CrudRepository<UserInfo, Integer> {
}
然后将两个Dao
类都注入到业务层中:
private final UserDao userDao;
private final RoleDao roleDao;
private final UserInfoDao userInfoDao;
@Autowired
public UserService(UserDao userDao, RoleDao roleDao, UserInfoDao userInfoDao) {
this.userDao = userDao;
this.roleDao = roleDao;
this.userInfoDao = userInfoDao;
}
现在业务层就用了持久化这三个东西的能力了。当然,实际上只需要持久化一个User
对象即可,Hibernate
会自动帮我们进行持久化,但还得编写查询Role
对象的方法比如getRoleByRoleName
,用于给控制器层组装UserData
,方法这里就省略了。至于和User
强一对一关系的UserInfo
,目前不需要单独查询。
业务层添加保存UserData
的方法
这个方法将UserData
中的数据拆解并且保存即可:
@Transactional
public UserData saveUserData(UserData userData) {
User user = userData.getUser();
userDao.save(user);
return userData;
}
由于Role
对象一定是已经存在的,所以不会重复保存。与User
强一对一关系的UserInfo
在类中已经设置过cascade = CascadeType.ALL
,所以会自动保存,因此这个方法中实际需要保存的只有一个User
对象。
简单测试
修改一下刚才用于测试的/account/test
控制器,组装一个UserData
对象,然后尝试保存:
@RequestMapping("/test")
public String test() {
System.out.println("组装一个Userdata");
User user = new User("gugugu" + System.currentTimeMillis(), passwordEncoder.encode(String.valueOf(System.currentTimeMillis())));
Role role = userService.getRoleByRoleName("USER");
List<Role> roles = new ArrayList<>();
roles.add(role);
user.setRoles(roles);
UserInfo userInfo = new UserInfo(String.valueOf(System.currentTimeMillis()) + "@sina.com");
user.setUserInfo(userInfo);
UserData userData = new UserData(user, roles, userInfo);
System.out.println(userData);
userService.saveUserData(userData);
return "redirect:/";
}
每次访问这个控制器,都会保存一个UserData
全套数据到数据库中,可见成功编写了。这里组装UserData
的代码显然会多次重复用到,就将其提取到两个私有方法中即可:
@RequestMapping("/test")
public String test() {
UserData userData = userService.saveUserData(
assembleUserData(
"gugu" + String.valueOf(System.currentTimeMillis()),
String.valueOf(System.currentTimeMillis()),
"ADMIN",
String.valueOf(System.currentTimeMillis()) + "@sina.com")
);
System.out.println("保存后的UserData是:");
System.out.println(userData);
return "redirect:/";
}
//组装UserData的私有方法,接受一个Role名称的集合
private UserData assembleUserData(String username, String password, List<String> roleNames, String email) {
User user = new User(username, passwordEncoder.encode(password));
List<Role> roles = roleNames.stream().map(userService::getRoleByRoleName).collect(Collectors.toList());
UserInfo userInfo = new UserInfo(email);
user.setRoles(roles);
user.setUserInfo(userInfo);
return new UserData(user, roles, userInfo);
}
//组装UserData的私有方法,接受单个Role名称
private UserData assembleUserData(String username, String password, String roleName, String email) {
List<String> roleNames = new ArrayList<>();
roleNames.add(roleName);
return assembleUserData(username, password, roleNames, email);
}
接下来准备编写用户注册功能的剩余部分,包括VO
,前端页面,控制器表单验证等。