这边是在学习了AOP和自定义注解之后,就想着将他们两个整合起来,以自定义注解进行标注,以AOP的反射获取信息,然后对代码进行加强,所以这边就简单的实现了一个进行邮箱参数格式校验的功能。
1.自定义注解
这边定义了两个自定义注解,一个是是否开启参数校验,另一个则是用来检查邮箱的格式是否符合规则的。至于这边为什么会用了两个注解,这个问题等到后面问题的时候再说。
- 1.1 开启参数校验的注解,这个注解就一个值isCheck(),该值为true时表示开启参数校验,为false时表示不开启,默认值为true
package com.mcj.music.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CheckParam {
//是否进行参数校验,默认是
boolean isCheck() default true;
}
- 1.2 用来检查邮箱格式是否符合规则的注解,该注解也就一个值msg(),用来表示当格式不符合规则时的提示信息,给了一个默认值,可以自己定义
package com.mcj.music.annotation;
import java.lang.annotation.*;
/**
* @author mcj
* @date 2022/10/28 20:27
* @description 邮箱参数校验有关的自定义注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
public @interface EmailFormatCheck {
// 当邮箱不符合规则时的提示信息
String msg() default "请输入正确的邮箱格式!";
}
2.定义切面
前面自定义注解已经定义好了,下面就该定义AOP的切面了。
package com.mcj.music.aspect;
import com.mcj.music.annotation.CheckParam;
import com.mcj.music.annotation.EmailFormatCheck;
import com.mcj.music.utils.Consts;
import com.mcj.music.utils.ResponseUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.lang.reflect.Parameter;
@Aspect
@Component
@Slf4j
public class ParamFormatCheckAspect {
@Pointcut("@annotation(com.mcj.music.annotation.CheckParam)")
public void checkParamPointcut(){}
/**
* 进行邮箱格式符合规则的校验
* @param joinPoint
*/
@Around("checkParamPointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取方法的参数
Object[] args = joinPoint.getArgs();
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Object proceed;
CheckParam annotation = methodSignature.getMethod().getAnnotation(CheckParam.class);
if (!annotation.isCheck()) {
proceed = joinPoint.proceed();
return proceed;
}
// 获取所有方法的参数名称
Parameter[] parameters = methodSignature.getMethod().getParameters();
// 遍历方法的参数名称
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
// 获取参数类型
Class<?> type = parameter.getType();
// 判断该参数是一个String类型
if (type == String.class) {
EmailFormatCheck emailFormatCheck = parameter.getAnnotation(EmailFormatCheck.class);
String arg = String.valueOf(args[i]);
// 判断参数的值是否符合邮箱格式
if (!arg.matches(Consts.EMAIL_FORMAT)) {
return ResponseUtils.fail(emailFormatCheck.msg());
}
continue;
}
// 判断是否是自定义类, classLoader等于null的时候不是自定义类
if (type.getClassLoader() != null) {
// 获取自定义对象的所有字段
Field[] declaredFields = type.getDeclaredFields();
Object arg = args[i];
for (Field declaredField : declaredFields) {
// 判断该字段上是否有注解
if (declaredField.getAnnotation(EmailFormatCheck.class) != null) {
EmailFormatCheck emailFormatCheck = declaredField.getAnnotation(EmailFormatCheck.class);
if (declaredField.getType() == String.class) {
// 取消字段的安全访问检查,使能够对字段进行操作
declaredField.setAccessible(true);
// 获取该字段的值
String fieldValue = String.valueOf(declaredField.get(arg));
// 检查该字段是否符合邮箱的格式
if (!fieldValue.matches(Consts.EMAIL_FORMAT)) {
return ResponseUtils.fail(emailFormatCheck.msg());
}
} else {
return ResponseUtils.fail("参数类型不正确!");
}
}
}
}
}
proceed = joinPoint.proceed();
return proceed;
}
}
PS:Consts.EMAIL_FORMAT:是我定义的一个常量值,是邮箱的格式的正则表达式。具体的值为:
// \u4e00-\u9fa5这个表示中文字符
^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$
ResponseUtils:这个是自定义的返回类型的工具类。
3.测试
上面自定义注解和切面已经定义好了,下面就是测试的代码类了,这边检查邮箱格式的注解是放在实体类的字段上,是否开启检查是放在controller的方法上面。
- 3.1 实体类
package com.mcj.music.domain;
import com.mcj.music.annotation.EmailFormatCheck;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
/**
* @author mcj
* @date 2022/10/9 9:34
* @description 管理员
*/
@ApiModel("管理员")
public class Admin implements Serializable {
/**
* id
*/
@ApiModelProperty("id")
private Integer id;
/**
* 账号
*/
@ApiModelProperty("账号")
private String name;
/**
* 密码
*/
@ApiModelProperty("密码")
private String password;
@ApiModelProperty("电子邮箱")
@EmailFormatCheck
private String email;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
- 3.2 controller方法
package com.mcj.music.controller;
import com.mcj.music.annotation.CheckParam;
import com.mcj.music.domain.Admin;
import com.mcj.music.service.AdminService;
import com.mcj.music.utils.ResponseUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @author mcj
* @date 2022/10/9 10:39
* @description 管理员controller
*/
@RestController
@Api(value = "管理员控制类", tags = "与管理员相关的接口")
public class AdminController {
@Autowired
private AdminService adminService;
/**
* 验证用户名和密码是否正确
* @param admin
* @return
*/
@PostMapping("/admin/login/status")
@ApiOperation(value = "验证用户名和密码是否正确", notes = "验证用户名和密码是否正确")
@CheckParam
public ResponseUtils loginStatus(@RequestBody Admin admin) {
ResponseUtils responseUtils;
String name = admin.getName();
String password = admin.getPassword();
boolean flag = adminService.verifyPassword(name, password);
if (flag) {
responseUtils = ResponseUtils.success("登陆成功");
} else {
responseUtils = ResponseUtils.fail("账号或密码错误");
}
return responseUtils;
}
}
- 3.3 运行结果
邮箱格式正确的结果截图:
邮箱格式错误的结果截图:
4.问题
- 4.1
问题:第一开始只使用了一个自定义注解@EmailFormatCheck将其作用在方法参数上,然后切点使用@annotation对这个注解进行拦截,发现怎么都拦截不到。
原因:这个问题的原因经过查找资料发现是因为@annotation不支持拦截作用在方法参数上的注解
解决:这个问题共有两种解决方案.
第一种就是使用execution()这种形式进行拦截,不过他这种好像针对拦截作用有固定参数个数的比较好,对于方法参数个数变化的表达式我这边不知道该怎么写.所以我这边使用了第二种。
第二种则是再加一个自定义注解,作用在方法上,然后用@annotation拦截这个作用在方法上的注解,随后通过反射获取方法的参数判断参数上是否有邮箱格式检查的注解,有的话在进行校验就行了。这个就是为什么我会用两个注解。 - 4.2
问题:将@EmailFormatCheck作用在自定义实体类的字段上,发现并没有对邮箱的格式进行校验。
原因:这个原因是自己在切面中,环绕通知中的代码逻辑写的有点问题,只是判断了一下该参数是否使用了@EmailFormatCheck注解,没有进行判断这个参数是否是实体类,然后实体类里面的字段是否使用了@EmailFormatCheck注解。
解决:这边是采用了type.getClassLoader() != null
的方法来判断是否是自定义类,如果是自定义类的话这边则会获取该自定义中每个字段,然后在判断该字段是否使用了@EmailFormatCheck注解,使用了该注解的话则进行相关的参数校验 - 4.3
问题:刚开始没有加declaredField.setAccessible(true);
时,使用declaredField.get(obj);
获取该字段的值会报错
原因:这是因为自定义实体类中的参数都是private的,所以这边不禁用掉它的安全检查,通过反射是无法获取该字段的值的。
解决:添加上declaredField.setAccessible(true);
这句代码禁用掉该字段的安全检查就行了。
5.总结
这边是利用自定义注解和AOP来简单的实现了一个邮箱格式校验的自定义注解。这边的实现主要也就是利用自定义注解做一个标注,然后利用aop中反射的使用来进行邮箱的校验、代码的增强。
相关 : https://www.bilibili.com/video/BV1jg4y1t7iw
https://www.bilibili.com/video/BV1DL411Q7ev