Fms-Java版开发实录:07 用户注册功能 - 数据对象和业务类编写

Fms-Java版开发实录:07 用户注册功能 - 数据对象和业务类编写

用户相关的URL已经编写完了4个,这次要为用户注册功能来编写查询和保存用户功能的业务层和持久层,以及重新设计用户数据。

用户相关的URL已经编写完了4个,这次继续来编写/account/register,也就是用户注册功能。不过首先还是看一下已经编写好的界面吧:

目前界面

登录页面:
fms-login

登录失败页面:
fms-loginerror

登出页面:
fms-logout

虽然没什么图片和炫酷设计,不过对于一个内部工具来说足够了。
下边继续编写用户注册功能,这一套编写好之后,其实未来的项目也可以直接拿过来使用。

数据对象设计

到目前为止没有用户注册功能,你可能会问猪哥是怎么登陆成功的,当然是我手工用Bcyrpt把密码塞到数据库里的,所以才能通过验证。编写好注册功能之后就不需要这么办了。

用户注册功能其实与其他的读写数据库功能没有什么不同,也是一个完整的CRUD操作,在此之前,还是需要好好设计一下,先来了解一下各种数据对象。

关于各种数据对象,我在CSDN上找了一张图:
fms-dataobjects
原文里写了各种数据对象的定义,我就不一一贴过来了。

针对用户注册,这里也需要先简单的设计一下这些类:
UserDataObjects
最下边是三个PO类,由于User有外键连接到另外两个表,所以通过获取User对象就可以得到已经存放在数据库内的某个用户的全部信息。目前获取用户的接口是UserAuthService,看来可以将其名称修改成UserService了。

然后从前端来看,目前用户提交的数据应该包含usernamepassword1password2email,这个很显然需要一个VO来验证并且存储经过验证之后的数据,就用UserRegistration作为这个VO

经过验证之后的数据,如果是新用户,那么还需要一个BO对象,这个对象应该是一个新的用户,其中应该包含一个新建的User对象,一个新建的UserInfo对象,以及已经确定的已经存在的List<Role>对象,即用户相关的全部内容,最后交给UserService来进行持久化。反过来说,UserService也需要返回一个UserData对象,未来在修改用户信息等场景的时候,就可以传递UserData对象给控制器。

完善PO类 - 编写UserInfo

目前已经有了UserRole这两个Entity类,也就是属于这里的PO类,即直接可以对应数据库记录的类。这两个类还需要实现Serializable接口,并且添加一个serialVersionUID如下:

    private static final long serialVersionUID = 1L;

我将User类的serialVersionUID设置为1LRole2L

之后为了取回密码等操作,实际上我打算还需要获取用户的电子邮箱地址,甚至未来可能还需要获取用户的其他信息,但是又不想增加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注解默认的FetchTypeEAGER,由于不是在全部情况下都需要UserInfo的数据,比如验证的时候就不需要,只有在修改或者重置密码的时候才需要,所以将其设置为LAZY;由于二者是强的一对一关系,所以把cascade设置为ALLoptional表示这一列不能为空。下边用@JoinColumn注解表示这是外键列,设置unique属性,这样就完成了外键列的设置。我的旧博客里关于Hibernate一对一映射的总结还是相当有用的。

再给User类补上GetterSetter方法,就完成了目前三个PO类的准备工作。此时必须把数据库里UserRole表都清空或者干脆删除,再重新启动项目,Hibernate就会自动创建表格。如果不清空,Hibernate会在添加外键列的时候报错,因为添加列的时候无法满足约束条件。在重新建表之后,可以手工塞入几个数据来测试一下,我目前的设计是:Role暂时分为ADMINSUPERUSERUSER三种。等编写到具体功能的时候再想想看如何对应这些角色。

继续往下之前,因为我们开始区分了POVOBO,就重新整理一下包,目前所有的PO位于cc.conyli.fms.entity包下,我们创建cc.conyli.fms.dataobject包,然后在其中创建entity包用于存放PO,创建valueobject存放VO类,businessobject存放BO类。利用IDEARefactor功能就可以很方便的完成。

编写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:/";
    }

访问这个路径就可以看到打印出的用户信息,如果将用户名换成不存在的用户,就可以看到如下图所示的错误页面:
fms-error

编写RoleUserInfoDao

这两个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,前端页面,控制器表单验证等。

LICENSED UNDER CC BY-NC-SA 4.0
Comment