注解的概念
在Spring Boot中,注解是一种用于提供元数据的特殊标记。它们可以应用于类、方法、字段或参数上,用于配置应用程序的行为或告诉Spring框架如何处理特定的组件。
Spring Boot提供了许多注解,用于简化配置和自动化常见任务。以下是一些常用的Spring Boot注解及其概念:
@SpringBootApplication
:这是一个组合注解,用于标记主应用程序类。它包含了@Configuration
、@EnableAutoConfiguration
和@ComponentScan
注解,用于启用自动配置和组件扫描。@RestController
:用于标记一个类,表示它是一个RESTful风格的控制器。它结合了@Controller
和@ResponseBody
注解,简化了编写RESTful API的过程。@RequestMapping
:用于映射HTTP请求到控制器的处理方法。可以用于类级别和方法级别,用于指定请求的URL路径和HTTP方法。@Autowired
:用于自动装配依赖。当Spring容器中存在匹配类型的bean时,它会自动将其注入到标记了@Autowired
的字段、构造函数或方法参数中。@Component
:用于标记一个类为Spring组件。Spring会自动扫描并将其实例化为bean,可以通过@Autowired
进行依赖注入。@Configuration
:用于标记一个类为配置类。配置类通常包含用于配置bean的方法,这些方法使用@Bean
注解进行标记。@EnableAutoConfiguration
:用于启用Spring Boot的自动配置机制。它会根据项目的依赖和配置,自动配置Spring应用程序的各种组件。
这只是一小部分Spring Boot注解的概念,Spring Boot提供了更多的注解用于不同的场景和需求。通过使用这些注解,可以简化配置和开发过程,提高开发效率。相当于一个说明文件,告诉应用程序某个被注解的类或属性是什么,要怎么处理。注解对于它所修饰的代码并没有直接的影响。
注解的使用范围
1)为编译器提供信息:注解能被编译器检测到错误或抑制警告。
2)编译时和部署时的处理: 软件工具能处理注解信息从而生成代码,XML文件等等。
3)运行时的处理:有些注解在运行时能被检测到。
自定义注解的步骤
第一步:定义注解
第二步:配置注解
第三步:解析注解
注解的基本语法
最基本的注解定义
public @interface MyAnnotation {
public String name();
int age();
String sex() default "女";
}
在自定义注解中,其实现部分只能定义注解类型元素!
说明:
- 访问修饰符必须为public,不写默认为public;
- 该元素的类型只能是基本数据类型、String、Class、枚举类型、注解类型以及一维数组;
- 该元素的名称一般定义为名词,如果注解中只有一个元素,名字起为value最好;
- ()不是定义方法参数的地方,也不能在括号中定义任何参数,仅仅只是一个特殊的语法;
- default`代表默认值,值必须定义的类型一致;
- 如果没有默认值,代表后续使用注解时必须给该类型元素赋值。
常用的元注解
元注解:专门修饰注解的注解。
@Target
@Target是专门用来限定某个自定义注解能够被应用在哪些Java元素上面的。其注解的源码如下
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* Returns an array of the kinds of elements an annotation type
* can be applied to.
* @return an array of the kinds of elements an annotation type
* can be applied to
*/
ElementType[] value();
}
从源码可以看出它使用一个枚举类型元素,接下来看这个枚举类型的源码
public enum ElementType {
/** Class, interface (including annotation type), or enum declaration(类、接口(包括注释类型)或枚举声明) */
TYPE,
/** Field declaration (includes enum constants) (字段声明(包括枚举常量))*/
FIELD,
/** Method declaration(方法声明) */
METHOD,
/** Formal parameter declaration(形式参数声明) */
PARAMETER,
/** Constructor declaration(构造函数声明) */
CONSTRUCTOR,
/** Local variable declaration(局部变量声明) */
LOCAL_VARIABLE,
/** Annotation type declaration(注释类型声明) */
ANNOTATION_TYPE,
/** Package declaration */
PACKAGE,
/**
* Type parameter declaration
*
* @since 1.8
*/
TYPE_PARAMETER,
/**
* Use of a type
*
* @since 1.8
*/
TYPE_USE
}
因此,我们可以在使用@Target时指定注解的使用范围,示例如下:
//@MyAnnotation被限定只能使用在类、接口或方法上面
@Target(value = {ElementType.METHOD,ElementType.TYPE})
public @interface MyAnnotation {
public String name();
int age();
String sex() default "女";
}
@Retention
@Retention注解,用来修饰自定义注解的生命力。
a.如果一个注解被定义为RetentionPolicy.SOURCE,则它将被限定在Java源文件中,那么这个注解即不会参与编译也不会在运行期起任何作用,这个注解就和一个注释是一样的效果,只能被阅读Java文件的人看到;
b.如果一个注解被定义为RetentionPolicy.CLASS,则它将被编译到Class文件中,那么编译器可以在编译时根据注解做一些处理动作,但是运行时JVM(Java虚拟机)会忽略它,我们在运行期也不能读取到,是默认的;
c.如果一个注解被定义为RetentionPolicy.RUNTIME,那么这个注解可以在运行期的加载阶段被加载到Class对象中。那么在程序运行阶段,我们可以通过反射得到这个注解,并通过判断是否有这个注解或这个注解中属性的值,从而执行不同的程序代码段。我们实际开发中的自定义注解几乎都是使用的RetentionPolicy.RUNTIME
。
@Retention注解源码如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}
里面也是一个枚举类型元素,其源码如下
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
*/
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
*/
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}
使用此注解修饰自定义注解生命力的示例如下:
//设置注解的生命力在运行期
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
public String name();
int age();
String sex() default "女";
}
@Documented
@Documented注解,是被用来指定自定义注解是否能随着被定义的java文件生成到JavaDoc文档当中。源码如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}
@Inherited
@Inherited注解,是指定某个自定义注解如果写在了父类的声明部分,那么子类(继承关系)的声明部分也能自动拥有该注解。该注解只对@Target被定义为ElementType.TYPE的自定义注解起作用。下面对其进行详细说明:
1)打开其源码,可以看到
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}
(1)该注解作用于整个程序运行中(@Retention(RetentionPolicy.RUNTIME);
(2)该注解只能修饰注解(@Target({ElementType.ANNOTATION_TYPE})),它是一个元注解。
此注解的中文翻译是继承的意思,那么究竟是什么意思呢?通过示例进行演示
第一:先定义两个注解@HasInherited 和 @NoInherited,前者注解包含@Inherited 注解,后者反之
也就是说,被 @Inherited 注解修饰的注解,如果作用于某个类上,其子类是可以继承的该注解的。否则,若一个注解没有被 @Inherited注解所修饰,那么其作用范围只能是当前类,其子类是不能被继承的。
自定义注解举例
创建注解
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.aidex.common.enums.BusinessType;
import com.aidex.common.enums.OperatorType;
/**
* 自定义操作日志记录注解
*
* @author wcy
*
*/
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log
{
/**
* 模块
*/
public String title() default "";
/**
* 功能
*/
public BusinessType businessType() default BusinessType.OTHER;
/**
* 操作人类别
*/
public OperatorType operatorType() default OperatorType.MANAGE;
/**
* 是否保存请求的参数
*/
public boolean isSaveRequestData() default true;
/**
* 是否保存响应的参数
*/
public boolean isSaveResponseData() default true;
}
创建aop切面
@Aspect
@Component
public class LogAspect {
private static final Logger LOG = LoggerFactory.getLogger(LogAspect.class);
private static final String LOG_IGNORE_PROPERTIES = "createBy,createDate,updateBy,updateDate,version,updateIp,delFlag,remoteAddr,requestUri,method,userAgent";
/** 排除敏感属性字段 */
public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };
// 配置织入点
@Pointcut("@annotation(com.aidex.common.annotation.Log)")
public void logPointCut() {
}
@Around("@annotation(com.aidex.common.annotation.Log)")
public Object loggingAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
// 定义返回对象、得到方法需要的参数
Object resultData = null;
Object[] args = joinPoint.getArgs();
long takeUpTime = 0;
resultData = joinPoint.proceed(args);
long endTime = System.currentTimeMillis();
takeUpTime = endTime - startTime;
handleLog(joinPoint, null, resultData, takeUpTime);
return resultData;
}
/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "logPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
handleLog(joinPoint, e, null, 0);
}
protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, long takeUpTime) {
try {
// 获得注解
Log controllerLog = getAnnotationLog(joinPoint);
if (controllerLog == null) {
return;
}
// 获取当前的用户
LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest());
// *========数据库日志=========*//
SysOperLog operLog = new SysOperLog();
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
// 请求的地址
String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
operLog.setOperIp(ip);
operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
if (loginUser != null) {
operLog.setOperName(loginUser.getUsername());
}
if (e != null) {
operLog.setStatus(BusinessStatus.FAIL.ordinal());
operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
// 设置请求方式
operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
// 处理设置注解上的参数
getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
operLog.setTakeUpTime(takeUpTime);
//设置数据变更
setLogContent(operLog);
// 保存数据库
AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
} catch (Exception exp) {
// 记录本地异常日志
LOG.error("异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
}
private void setLogContent(SysOperLog operLog) {
if (ContextHandler.get(BaseEntity.LOG_TYPE) != null) {
String logType = (String) ContextHandler.get(BaseEntity.LOG_TYPE);
if (StringUtils.isNotBlank(logType)) {
if ("insert".equals(logType)) {
// 异步保存日志
} else if ("update".equals(logType)) {
} else if ("delete".equals(logType)) {
}
}
}
}
/**
* 获取注解中对方法的描述信息 用于Controller层注解
*
* @param log 日志
* @param operLog 操作日志
* @throws Exception
*/
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception {
// 设置action动作
operLog.setBusinessType(log.businessType().ordinal());
// 设置标题
operLog.setTitle(log.title());
// 设置操作人类别
operLog.setOperatorType(log.operatorType().ordinal());
// 是否需要保存request,参数和值
if (log.isSaveRequestData()) {
// 获取参数的信息,传入到数据库中。
setRequestValue(joinPoint, operLog);
}
// 是否需要保存response,参数和值
if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) {
operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
}
}
/**
* 获取请求的参数,放到log中
*
* @param operLog 操作日志
* @throws Exception 异常
*/
private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception {
String requestMethod = operLog.getRequestMethod();
if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
String params = argsArrayToString(joinPoint.getArgs());
operLog.setOperParam(StringUtils.substring(params, 0, 2000));
} else {
Map<?, ?> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
operLog.setOperParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter()), 0, 2000));
}
}
/**
* 是否存在注解,如果存在就获取
*/
private Log getAnnotationLog(JoinPoint joinPoint) throws Exception {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null) {
return method.getAnnotation(Log.class);
}
return null;
}
/**
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray) {
String params = "";
if (paramsArray != null && paramsArray.length > 0) {
for (Object o : paramsArray) {
if (StringUtils.isNotNull(o) && !isFilterObject(o)) {
try {
String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter());
params += jsonObj.toString() + " ";
} catch (Exception e) {
}
}
}
}
return params.trim();
}
/**
* 忽略敏感属性
*/
public PropertyPreExcludeFilter excludePropertyPreFilter()
{
return new PropertyPreExcludeFilter().addExcludes(EXCLUDE_PROPERTIES);
}
/**
* 判断是否需要过滤的对象。
*
* @param o 对象信息。
* @return 如果是需要过滤的对象,则返回true;否则返回false。
*/
@SuppressWarnings("rawtypes")
public boolean isFilterObject(final Object o) {
Class<?> clazz = o.getClass();
if (clazz.isArray()) {
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o;
for (Object value : collection) {
return value instanceof MultipartFile;
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
for (Object value : map.entrySet()) {
Map.Entry entry = (Map.Entry) value;
return entry.getValue() instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|| o instanceof BindingResult;
}
}
结合SpEl实现注解动态参数传值
在自定义注解后,使用注解时其参数必须是固定不变的。那么能不能实现动态传参呢?答案是肯定的。
这时便可以结合SpEL表达式+注解方式实现。简单来说,就是在查阅文档时,需要传入用户的编号,把用户编号作为权限注解的参数,在切面中去获取用户编号去验证此用户是否有查阅权限,如果有则放行,反之则抛出异常,进行全局异常捕获后指定返回的信息。相比于拦截器,个人更推荐这种方式。具体实现如下:
自定义注解
/**
* 资源访问权限注解
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResourceAccessPermission {
/**
* 用户编号,可支持SpEL表达式
*
* @return
*/
String value();
}
创建SpEL表达式解析器
/**
* SpEL表达式解析器,根据传入的类型进行解析
*
* @param <T>
*/
public class ExpressionEvaluator<T> extends CachedExpressionEvaluator {
private final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer();
private final Map<ExpressionKey, Expression> conditionCache = new ConcurrentHashMap<>(64);
private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<>(64);
/**
* 创建表达式上下文
*
* @param object
* @param targetClass
* @param method
* @param args
* @return
*/
public EvaluationContext createEvaluationContext(Object object, Class<?> targetClass, Method method, Object[] args) {
Method targetMethod = getTargetMethod(targetClass, method);
ExpressionRootObject root = new ExpressionRootObject(object, args);
return new MethodBasedEvaluationContext(root, targetMethod, args, this.paramNameDiscoverer);
}
/**
* 获取目标对象的方法
*
* @param targetClass
* @param method
* @return
*/
private Method getTargetMethod(Class<?> targetClass, Method method) {
AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
Method targetMethod = this.targetMethodCache.get(methodKey);
if (targetMethod == null) {
targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
this.targetMethodCache.put(methodKey, targetMethod);
}
return targetMethod;
}
/**
* 根据条件表达式获SpEL取值
*
* @param conditionExpression
* @param elementKey
* @param evalContext
* @param clazz
* @return
*/
public T getValueByConditionExpression(String conditionExpression, AnnotatedElementKey elementKey, EvaluationContext evalContext, Class<T> clazz) {
return getExpression(this.conditionCache, elementKey, conditionExpression).getValue(evalContext, clazz);
}
}
@Getter
@ToString
@AllArgsConstructor
class ExpressionRootObject {
private final Object object;
private final Object[] args;
}
配置切面,进行权限认证
/**
* 资源访问权限切面处理
*/
@Component
@Aspect
@Slf4j
public class ResourceAccessPermissionAspect {
private ExpressionEvaluator<String> evaluator = new ExpressionEvaluator<>();
@Pointcut("@annotation(com.zxh.test.annotation.ResourceAccessPermission)")
private void pointCut() {
}
@Before("pointCut()")
public void doPermission(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
Object[] args = joinPoint.getArgs();
ResourceAccessPermission permission = method.getAnnotation(ResourceAccessPermission.class);
if (args == null) {
return;
}
// 读取参数,如果以#开头则按照EL处理,否则按照普通字符串处理
String userNo = permission.value();
if (StrUtil.startWith(userNo, "#")) {
Object pointTarget = joinPoint.getTarget();
Class<?> aClass = pointTarget.getClass();
// SpEL表达式的方式读取对应参数值
EvaluationContext evaluationContext = evaluator.createEvaluationContext(pointTarget, aClass, method, args);
AnnotatedElementKey methodKey = new AnnotatedElementKey(method, aClass);
//根据条件读取注解中的参数值
userNo = evaluator.getValueByConditionExpression(userNo, methodKey, evaluationContext, String.class);
}
log.info("参数:{}", userNo);
// TODO 按照业务自定义逻辑处理
/**
* 这里为了演示,假设用户编号不等于3时显示无权限,实际需要去查询用户与权限分配表
* 不符合要求的就抛出指定的异常,再捕获全局异常返回相应提示信息即可
*/
if (!userNo.equals("3")) {
throw new RuntimeException("无权访问");
}
}
}
接口调用
/**
* 这里将查询条件中的用户编号作为参数传入注解
*
* @param queryBody
* @return
*/@PostMapping("/read")
@ResourceAccessPermission("#queryBody.userNo")
public String read(@RequestBody QueryBody queryBody) {
return "查询成功";
}
此处评论已关闭