我有两个彼此靠近的控制器——一个用于产品,一个用于用户。我想将Spring Validation添加到两个控制器中。所以我添加了实现Validator的ProductValidator和UserValidator类。ProductValidator运行良好。问题是从未调用UserValidator。正在调用InitBinder方法,但验证不起作用。与此同时,ProductController中的产品运行良好。
这是用户逻辑的代码。
用户控制器
@Controller
@RequestMapping(path = "/users")
public class UserController {
private final UserService userService;
private final RoleService roleService;
private PasswordEncoder passwordEncoder;
@Autowired
public UserController(UserService userService, RoleService roleService, PasswordEncoder passwordEncoder) {
this.userService = userService;
this.roleService = roleService;
this.passwordEncoder = passwordEncoder;
}
@Autowired
@Qualifier("userValidator")
private UserValidator userValidator;
@InitBinder("user")
private void initBinder(WebDataBinder binder) {
binder.setValidator(userValidator);
binder.registerCustomEditor(Role.class, "roles", new RoleEditor(roleService));
}
...
@RequestMapping(path = "/save", method = RequestMethod.POST)
public ModelAndView submit(@ModelAttribute("user") @Valid User user,
BindingResult result) {
...
}
...
}
用户验证器
@Component("userValidator")
public class UserValidator implements Validator {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
@Autowired
public UserValidator(UserRepository userRepository, RoleRepository roleRepository) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
}
@Override
public boolean supports(Class<?> paramClass) {
return User.class.equals(paramClass);
}
@Override
public void validate(Object obj, Errors errors) {
User user = (User) obj;
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "username", "username.required", "Enter username");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "firstName.required", "Enter user first name");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "lastName.required", "Enter user last name");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "password.required", "Enter user password");
User userToFind = userRepository.findByUsername(user.getUsername()).orElse(new User());
if (Objects.isNull(user.getId()) && Objects.nonNull(userToFind.getId())) {
errors.rejectValue("username", "username.already.exist", "user with this username already exists");
}
if (Objects.isNull(user.getRoles())) {
errors.rejectValue("roles", "role.required", "role must be assigned");
}
Role adminRole = roleRepository.getAdminRole();
if (Objects.nonNull(user.getId())
&& Objects.nonNull(user.getRoles())
&& !user.getRoles().contains(adminRole)
&& userRepository.findUsersWithAdministratorRole().size() == 1) {
errors.rejectValue("roles", "admin.required", "at least one admin role must exist");
}
}
public ErrorMessage validateUserToDelete(UUID id) {
ErrorMessage errorMessage = new ErrorMessage();
List<String> errors = new ArrayList<>();
Set<User> admins = userRepository.findUsersWithAdministratorRole();
if (admins.size() == 1) {
errors.add(String.format("User with email %s is the one with Admin role. Impossible to delete last Admin user."
, userRepository.findById(id).get().getUsername()));
}
errorMessage.setErrors(errors);
return errorMessage;
}
}
用户
@Entity
@Table(name = "users")
public class User {
private UUID id;
private String username;
private String password;
private String firstName;
private String lastName;
private BigDecimal money;
private Set<Role> roles;
private Set<Product> products;
@Id
@Type(type="org.hibernate.type.PostgresUUIDType")
@Column(name = "id", columnDefinition = "uuid")
@GeneratedValue(strategy = GenerationType.AUTO)
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
@Column(name = "username")
@NotEmpty
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Column(name = "password")
@NotEmpty
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Column(name = "first_name")
@NotEmpty
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
@Column(name = "last_name")
@NotEmpty
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
@Column(name = "money")
@DecimalMin(value = "0.0", inclusive = false)
@Digits(integer = 10, fraction = 2)
public BigDecimal getMoney() {
return money;
}
public void setMoney(BigDecimal money) {
this.money = money;
}
@ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.MERGE, CascadeType.REFRESH})
@JoinTable(
name = "users_roles",
joinColumns = { @JoinColumn(name = "user_id") },
inverseJoinColumns = { @JoinColumn(name = "role_id") }
)
@NotNull
public Set<Role> getRoles() {
return roles;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
@ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.MERGE, CascadeType.REFRESH})
@JoinTable(
name = "checkout",
joinColumns = { @JoinColumn(name = "user_id") },
inverseJoinColumns = { @JoinColumn(name = "product_id") }
)
public Set<Product> getProducts() {
return products;
}
public void setProducts(Set<Product> products) {
this.products = products;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return id.equals(user.id) && username.equals(user.username) && password.equals(user.password) && firstName.equals(user.firstName) && lastName.equals(user.lastName);
}
@Override
public int hashCode() {
return Objects.hash(id, username, password, firstName, lastName);
}
}
user.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<c:set var="contextPath" value="${pageContext.request.contextPath}"/>
<!DOCTYPE html>
<html>
<head>
<c:import url="${contextPath}/WEB-INF/jsp/header.jsp"/>
</head>
<body>
<c:import url="${contextPath}/WEB-INF/jsp/navibar.jsp"/>
<div class="container">
<div class="row">
<security:authorize access="hasRole('ROLE_ADMIN')">
<div class="btn-toolbar" role="toolbar" aria-label="Toolbar with button groups">
<div class="btn-group me-2" role="group" aria-label="Second group">
<a href="/users" type="button" class="btn btn-success">Back to users</a>
</div>
</div>
</security:authorize>
</div><br>
<form:form action="/users/save" method="post" modelAttribute="user">
<div class="form-group">
<div class="row">
<form:label path="id">User ID:</form:label><br>
<form:input path="id" type="UUID" readonly="true" class="form-control" id="id" placeholder="User ID" name="id" value="${user.id}"/>
<br>
<form:label path="username">username (email address):</form:label><br>
<form:input path="username" type="text" class="form-control" id="username" placeholder="Enter username" name="username" value="${user.username}"/>
<form:errors path="username" cssClass="error"/>
<br>
<form:label path="password">Password:</form:label><br>
<form:input path="password" type="text" class="form-control" id="password" placeholder="Enter password" name="password" value="${user.password}"/>
<form:errors path="password" cssClass="error"/>
<br>
<form:label path="firstName">First name:</form:label><br>
<form:input path="firstName" type="text" class="form-control" id="firstName" placeholder="Enter first name" name="firstName" value="${user.firstName}"/>
<form:errors path="firstName" cssClass="error"/>
<br>
<form:label path="lastName">First name:</form:label><br>
<form:input path="lastName" type="text" class="form-control" id="lastName" placeholder="Enter last name" name="lastName" value="${user.lastName}"/>
<form:errors path="lastName" cssClass="error"/>
<br>
<form:label path="money">Money:</form:label><br>
<form:input path="money" type="number" class="form-control" id="money" placeholder="Enter money" name="money" value="${user.money}"/>
<form:errors path="money" cssClass="error"/>
<br>
<security:authorize access="hasRole('ROLE_ADMIN')">
<form:label path="roles">Roles:</form:label><br>
<c:forEach items="${roles}" var="role">
<form:checkbox path="roles" id="${roles}" label="${role.name}" value="${role}"/></td>
</c:forEach>
<br>
<form:errors path="roles" cssClass="error"/><br><br>
</security:authorize>
</div>
<div class="row">
<div class="btn-toolbar" role="toolbar" aria-label="Toolbar with button groups">
<div class="btn-group me-2" role="group" aria-label="Second group">
<form:button type="submit" value="Submit" class="btn btn-primary">Save</form:button>
</div>
</div>
</div>
</div>
</form:form>
</div>
</body>
</html>
如果查看调试器com.intellias.testmarketplace.controller.UserController$$EnhancerBySpringCGLIB$$309171b7
中的类名,就会看到$$EnhancerBySpringCGLIB
。该部分表示Spring正在生成一个代理。可以出于各种原因生成代理,但控制器最常见的原因是安全性,@PreAuthorize
注释会出现在脑海中。另一个选项是@Timed
注释,用于对控制器进行监视。
由于initBinder
方法是private
,因此它将直接在代理上调用,而不是在实际的bean实例上调用。这是由于创建了一个类代理,它通过扩展类来工作。正在生成一个具有相同签名的方法,并调用拦截器,然后将调用传递给实际方法。但是,这只适用于public
和protected
方法,而不适用于private
或final
方法。这些将在空代理上直接调用(字段为null
,因为它不需要依赖项(。
由于您的方法是private
,因此会在代理上调用它。代理没有设置字段,因此userValidator
是null
。因此,有效地,WebDataBinder
上没有设置Validator
,因此不会也不能进行验证。
要修复此问题,请将方法设为public
(或protected
(,以便在实际bean而不是代理上正确调用它。
对于未来,根据经验,您可能希望控制器中带有注释的方法无论如何都是public
,而不是"私有"。
问题出现在initBinder
方法的私有访问修饰符中,以及一些UserController
方法上的@PreAuthorize
注释中。非常感谢@M.Deinum的耐心和帮助。