当前位置: 首页 > news >正文

java分组接口及校验注解用法

分组接口(Group Interface)是 Bean Validation(JSR 380) 提供的一种机制,用于在参数校验时动态选择需要校验的字段或规则。通过分组接口,可以将校验逻辑按业务场景划分,从而实现更灵活的参数校验。

以下是分组接口的技术细节和实现原理:


1. 分组接口的定义

分组接口是一个普通的 Java 接口,没有任何方法。它的作用仅仅是作为一个标记(Marker Interface),用于标识不同的校验场景。

public interface GroupA {}

public interface GroupB {}

2. 在字段上指定分组

在校验注解(如 @NotEmpty@Size 等)中,可以通过 groups 属性指定该注解所属的分组。

public class User {

    @NotEmpty(message = "用户名不能为空", groups = GroupA.class)
    private String username;

    @Size(min = 6, max = 20, message = "密码长度必须在6到20之间", groups = GroupB.class)
    private String password;
}
  • username 字段的 @NotEmpty 校验规则属于 GroupA 分组。
  • password 字段的 @Size 校验规则属于 GroupB 分组。

3. 在 Controller 中使用分组校验

在 Spring MVC 中,可以通过 @Validated 注解指定校验分组。

@RestController
public class UserController {

    @PostMapping("/createUser")
    public ResponseEntity<?> createUser(@Validated(GroupA.class) @RequestBody User user) {
        // 只校验属于 GroupA 分组的字段
        return ResponseEntity.ok("用户创建成功");
    }

    @PostMapping("/updateUser")
    public ResponseEntity<?> updateUser(@Validated({GroupA.class, GroupB.class}) @RequestBody User user) {
        // 校验属于 GroupA 和 GroupB 分组的字段
        return ResponseEntity.ok("用户更新成功");
    }
}
  • createUser 方法中,只会校验 username 字段(因为 username 属于 GroupA 分组)。
  • updateUser 方法中,会同时校验 usernamepassword 字段(因为它们分别属于 GroupAGroupB 分组)。

4. 默认分组

如果没有指定 groups 属性,校验注解默认属于 javax.validation.groups.Default 分组。

@NotEmpty(message = "用户名不能为空") // 默认属于 Default 分组
private String username;

5. 分组继承

分组接口可以继承其他接口,从而实现分组的组合。

public interface GroupA {}

public interface GroupB extends GroupA {}
  • 如果一个字段的校验注解属于 GroupB 分组,那么它也会被 GroupA 分组校验。

6. 动态分组

分组校验可以根据业务逻辑动态选择。例如,根据系统配置决定是否启用某些校验规则。

@PostMapping("/login")
public ResponseEntity<?> login(@Validated({Default.class, CodeEnableGroup.class}) @RequestBody AuthLoginReqVO reqVO) {
    // 根据系统配置动态选择分组
    if (enableCaptcha) {
        // 启用验证码校验
        validator.validate(reqVO, CodeEnableGroup.class);
    }
    return ResponseEntity.ok("登录成功");
}

7. 自定义校验器与分组

如果你实现了自定义校验器(通过 ConstraintValidator),可以在校验器中获取当前的分组信息。

public class CustomValidator implements ConstraintValidator<CustomAnnotation, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 获取当前的分组信息
        Set<Class<?>> groups = context.getConstraintDescriptor().getGroups();
        if (groups.contains(GroupA.class)) {
            // 执行 GroupA 分组的校验逻辑
        }
        return true;
    }
}

8. 分组校验的实现原理

Spring Validation 是基于 Bean Validation(Hibernate Validator)实现的。分组校验的核心原理如下:

  1. 注解解析

    • Spring 会解析 @Validated 注解中指定的分组。
    • 根据分组信息,筛选出需要校验的字段和规则。
  2. 校验器调用

    • Spring 调用 Bean Validation 的校验器(如 Hibernate Validator),并传入分组信息。
    • 校验器根据分组信息执行相应的校验逻辑。
  3. 校验结果处理

    • 如果校验失败,Spring 会抛出 MethodArgumentNotValidException 异常,并返回错误信息。

9. 分组校验的优缺点

优点
  • 灵活性:可以根据业务场景动态选择校验规则。
  • 解耦:将校验逻辑与业务逻辑分离,代码更清晰。
  • 复用性:同一个字段可以在不同的分组中复用,避免重复定义。
缺点
  • 复杂度增加:如果分组过多,可能会导致代码复杂度增加。
  • 维护成本:需要仔细管理分组接口,避免分组冲突或遗漏。

10. 最佳实践

  1. 按业务场景划分分组

    • 将分组接口与具体的业务场景对应,例如 CreateGroupUpdateGroupLoginGroup 等。
  2. 避免过度分组

    • 不要为每个字段都创建单独的分组,而是将相关的字段划分到同一个分组中。
  3. 使用默认分组

    • 对于通用的校验规则,可以使用 Default 分组,避免重复定义。
  4. 分组继承

    • 通过分组继承实现分组的组合,减少重复代码。

总结

分组接口是 Bean Validation 提供的一种强大机制,用于实现动态、灵活的参数校验。通过分组接口,可以根据业务场景选择需要校验的字段和规则,从而提高代码的可维护性和复用性。在实际开发中,合理使用分组接口可以显著提升参数校验的灵活性和可读性。

package cn.iocoder.yudao.module.system.controller.admin.auth.vo;

import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;

@Schema(description = "管理后台 - 账号密码登录 Request VO,如果登录并绑定社交用户,需要传递 social 开头的参数")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AuthLoginReqVO {

    @Schema(description = "账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudaoyuanma")
    @NotEmpty(message = "登录账号不能为空")
    @Length(min = 4, max = 16, message = "账号长度为 4-16 位")
    @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
    private String username;

    @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao")
    @NotEmpty(message = "密码不能为空")
    @Length(min = 4, max = 16, message = "密码长度为 4-16 位")
    private String password;

    // ========== 图片验证码相关 ==========

    @Schema(description = "验证码,验证码开启时,需要传递", requiredMode = Schema.RequiredMode.REQUIRED,
            example = "PfcH6mgr8tpXuMWFjvW6YVaqrswIuwmWI5dsVZSg7sGpWtDCUbHuDEXl3cFB1+VvCC/rAkSwK8Fad52FSuncVg==")
    @NotEmpty(message = "验证码不能为空", groups = CodeEnableGroup.class)
    private String captchaVerification;

    // ========== 绑定社交登录时,需要传递如下参数 ==========

    @Schema(description = "社交平台的类型,参见 SocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    @InEnum(SocialTypeEnum.class)
    private Integer socialType;

    @Schema(description = "授权码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    private String socialCode;

    @Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62")
    private String socialState;

    /**
     * 开启验证码的 Group
     */
    public interface CodeEnableGroup {}

    @AssertTrue(message = "授权码不能为空")
    public boolean isSocialCodeValid() {
        return socialType == null || StrUtil.isNotEmpty(socialCode);
    }

    @AssertTrue(message = "授权 state 不能为空")
    public boolean isSocialState() {
        return socialType == null || StrUtil.isNotEmpty(socialState);
    }

}
package cn.iocoder.yudao.module.system.service.auth;

import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO;
import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*;
import cn.iocoder.yudao.module.system.convert.auth.AuthConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import cn.iocoder.yudao.module.system.enums.logger.LoginLogTypeEnum;
import cn.iocoder.yudao.module.system.enums.logger.LoginResultEnum;
import cn.iocoder.yudao.module.system.enums.oauth2.OAuth2ClientConstants;
import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum;
import cn.iocoder.yudao.module.system.service.logger.LoginLogService;
import cn.iocoder.yudao.module.system.service.member.MemberService;
import cn.iocoder.yudao.module.system.service.oauth2.OAuth2TokenService;
import cn.iocoder.yudao.module.system.service.social.SocialUserService;
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import com.google.common.annotations.VisibleForTesting;
import com.xingyuv.captcha.model.common.ResponseModel;
import com.xingyuv.captcha.model.vo.CaptchaVO;
import com.xingyuv.captcha.service.CaptchaService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import javax.validation.Validator;
import java.util.Objects;

import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;

/**
 * Auth Service 实现类
 *
 * @author 芋道源码
 */
@Service
@Slf4j
public class AdminAuthServiceImpl implements AdminAuthService {

    @Resource
    private AdminUserService userService;
    @Resource
    private LoginLogService loginLogService;
    @Resource
    private OAuth2TokenService oauth2TokenService;
    @Resource
    private SocialUserService socialUserService;
    @Resource
    private MemberService memberService;
    @Resource
    private Validator validator;
    @Resource
    private CaptchaService captchaService;
    @Resource
    private SmsCodeApi smsCodeApi;

    /**
     * 验证码的开关,默认为 true
     */
    @Value("${yudao.captcha.enable:true}")
    private Boolean captchaEnable;

    @Override
    public AdminUserDO authenticate(String username, String password) {
        final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME;
        // 校验账号是否存在
        AdminUserDO user = userService.getUserByUsername(username);
        if (user == null) {
            createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
            throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
        }
        if (!userService.isPasswordMatch(password, user.getPassword())) {
            createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
            throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
        }
        // 校验是否禁用
        if (CommonStatusEnum.isDisable(user.getStatus())) {
            createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.USER_DISABLED);
            throw exception(AUTH_LOGIN_USER_DISABLED);
        }
        return user;
    }

    @Override
    public AuthLoginRespVO login(AuthLoginReqVO reqVO) {
        // 校验验证码
        validateCaptcha(reqVO);

        // 使用账号密码,进行登录
        AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword());

        // 如果 socialType 非空,说明需要绑定社交用户
        if (reqVO.getSocialType() != null) {
            socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
                    reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()));
        }
        // 创建 Token 令牌,记录登录日志
        return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME);
    }

    @Override
    public void sendSmsCode(AuthSmsSendReqVO reqVO) {
        // 登录场景,验证是否存在
        if (userService.getUserByMobile(reqVO.getMobile()) == null) {
            throw exception(AUTH_MOBILE_NOT_EXISTS);
        }
        // 发送验证码
        smsCodeApi.sendSmsCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP()));
    }

    @Override
    public AuthLoginRespVO smsLogin(AuthSmsLoginReqVO reqVO) {
        // 校验验证码
        smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.ADMIN_MEMBER_LOGIN.getScene(), getClientIP()));

        // 获得用户信息
        AdminUserDO user = userService.getUserByMobile(reqVO.getMobile());
        if (user == null) {
            throw exception(USER_NOT_EXISTS);
        }

        // 创建 Token 令牌,记录登录日志
        return createTokenAfterLoginSuccess(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE);
    }

    private void createLoginLog(Long userId, String username,
                                LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) {
        // 插入登录日志
        LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
        reqDTO.setLogType(logTypeEnum.getType());
        reqDTO.setTraceId(TracerUtils.getTraceId());
        reqDTO.setUserId(userId);
        reqDTO.setUserType(getUserType().getValue());
        reqDTO.setUsername(username);
        reqDTO.setUserAgent(ServletUtils.getUserAgent());
        reqDTO.setUserIp(ServletUtils.getClientIP());
        reqDTO.setResult(loginResult.getResult());
        loginLogService.createLoginLog(reqDTO);
        // 更新最后登录时间
        if (userId != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) {
            userService.updateUserLogin(userId, ServletUtils.getClientIP());
        }
    }

    @Override
    public AuthLoginRespVO socialLogin(AuthSocialLoginReqVO reqVO) {
        // 使用 code 授权码,进行登录。然后,获得到绑定的用户编号
        SocialUserRespDTO socialUser = socialUserService.getSocialUserByCode(UserTypeEnum.ADMIN.getValue(), reqVO.getType(),
                reqVO.getCode(), reqVO.getState());
        if (socialUser == null || socialUser.getUserId() == null) {
            throw exception(AUTH_THIRD_LOGIN_NOT_BIND);
        }

        // 获得用户
        AdminUserDO user = userService.getUser(socialUser.getUserId());
        if (user == null) {
            throw exception(USER_NOT_EXISTS);
        }

        // 创建 Token 令牌,记录登录日志
        return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL);
    }

    @VisibleForTesting
    void validateCaptcha(AuthLoginReqVO reqVO) {
        // 如果验证码关闭,则不进行校验
        if (!captchaEnable) {
            return;
        }
        // 校验验证码
        ValidationUtils.validate(validator, reqVO, AuthLoginReqVO.CodeEnableGroup.class);
        CaptchaVO captchaVO = new CaptchaVO();
        captchaVO.setCaptchaVerification(reqVO.getCaptchaVerification());
        ResponseModel response = captchaService.verification(captchaVO);
        // 验证不通过
        if (!response.isSuccess()) {
            // 创建登录失败日志(验证码不正确)
            createLoginLog(null, reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME, LoginResultEnum.CAPTCHA_CODE_ERROR);
            throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR, response.getRepMsg());
        }
    }

    private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) {
        // 插入登陆日志
        createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS);
        // 创建访问令牌
        OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(),
                OAuth2ClientConstants.CLIENT_ID_DEFAULT, null);
        // 构建返回结果
        return AuthConvert.INSTANCE.convert(accessTokenDO);
    }

    @Override
    public AuthLoginRespVO refreshToken(String refreshToken) {
        OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT);
        return AuthConvert.INSTANCE.convert(accessTokenDO);
    }

    @Override
    public void logout(String token, Integer logType) {
        // 删除访问令牌
        OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.removeAccessToken(token);
        if (accessTokenDO == null) {
            return;
        }
        // 删除成功,则记录登出日志
        createLogoutLog(accessTokenDO.getUserId(), accessTokenDO.getUserType(), logType);
    }

    private void createLogoutLog(Long userId, Integer userType, Integer logType) {
        LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
        reqDTO.setLogType(logType);
        reqDTO.setTraceId(TracerUtils.getTraceId());
        reqDTO.setUserId(userId);
        reqDTO.setUserType(userType);
        if (ObjectUtil.equal(getUserType().getValue(), userType)) {
            reqDTO.setUsername(getUsername(userId));
        } else {
            reqDTO.setUsername(memberService.getMemberUserMobile(userId));
        }
        reqDTO.setUserAgent(ServletUtils.getUserAgent());
        reqDTO.setUserIp(ServletUtils.getClientIP());
        reqDTO.setResult(LoginResultEnum.SUCCESS.getResult());
        loginLogService.createLoginLog(reqDTO);
    }

    private String getUsername(Long userId) {
        if (userId == null) {
            return null;
        }
        AdminUserDO user = userService.getUser(userId);
        return user != null ? user.getUsername() : null;
    }

    private UserTypeEnum getUserType() {
        return UserTypeEnum.ADMIN;
    }

    @Override
    public AuthLoginRespVO register(AuthRegisterReqVO registerReqVO) {
        // 1. 校验验证码
        validateCaptcha(registerReqVO);

        // 2. 校验用户名是否已存在
        Long userId = userService.registerUser(registerReqVO);

        // 3. 创建 Token 令牌,记录登录日志
        return createTokenAfterLoginSuccess(userId, registerReqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME);
    }

    @VisibleForTesting
    void validateCaptcha(AuthRegisterReqVO reqVO) {
        // 如果验证码关闭,则不进行校验
        if (!captchaEnable) {
            return;
        }
        // 校验验证码
        ValidationUtils.validate(validator, reqVO, AuthLoginReqVO.CodeEnableGroup.class);
        CaptchaVO captchaVO = new CaptchaVO();
        captchaVO.setCaptchaVerification(reqVO.getCaptchaVerification());
        ResponseModel response = captchaService.verification(captchaVO);
        // 验证不通过
        if (!response.isSuccess()) {
            throw exception(AUTH_REGISTER_CAPTCHA_CODE_ERROR, response.getRepMsg());
        }
    }

}

相关文章:

  • 查看notebook的jupyter token
  • 说说高级java每日一道面试题-2025年2月10日-AOP篇-Spring AOP和AspectJ,AOP有什么区别?
  • 信息安全工程师-快速记忆GB17859中的五个安全保护等级
  • 【推荐】碰一碰发视频源码搭建,支持OEM
  • Oracle 19C Database Data Guard 一主两备 -- 生产级
  • Trader Joe‘s EDI 需求分析
  • k8s集群离线安装kuberay operator
  • 单智能体到多智能体智能体任务规划有什么变化
  • 深入浅出:机器学习的全面解析
  • 加油口,电梯门的对称性对 TCP/IP 传输协议的启示
  • 智慧城市V4系统小程序源码独立版全插件全开源
  • Pro Git --(Windows)总结
  • 网络安全防范
  • 雪花算法生成唯一ID并解决时钟回拨问题实战
  • vue3+vite项目引入electron运行为桌面项目
  • fork: retry: No child processes-linux18
  • Kotlin 2.1.0 入门教程(十七)接口
  • SpringCloud面试题----微服务下为什需要链路追踪系统
  • 2月14(信息差)
  • 【算法工程】解决linux下Aspose.slides提示No usable version of libssl found以及强化推理模型的短板
  • 南阳网站推广效果/福建seo关键词优化外包
  • 酒类网站建设/lol关键词查询
  • 宿迁沭阳网站建设/网推一手单渠道
  • 常熟做公司网站/网站排名优化公司哪家好
  • 帮客户做ppt什么的在哪个网站/百度官方认证
  • 南山网站制作联系电话/怎样优化网络