深入探讨 Spring 中的自定义注解及其使用场景

在现代 Java 开发中,注解(Annotation)作为一种元数据形式,极大地简化了开发者的代码配置和逻辑实现。Spring 框架充分利用了注解的功能,实现了依赖注入、事务管理、AOP(面向切面编程)等核心特性。随着项目复杂度的增加,开发者可能会发现需要定义一些自定义注解,以满足更灵活的业务需求。

本文将深入探讨在 Spring 框架中如何创建和使用自定义注解,并结合具体的应用场景给出实际的示例,帮助开发者理解自定义注解的价值和最佳实践。

一、什么是注解

注解(Annotation)是 Java 5 引入的一种元数据机制,用来为代码中的类、方法、字段、参数等提供额外的信息。注解不会直接影响代码的执行,但可以通过反射或编译器进行处理。Spring 框架通过注解简化了配置和开发,例如常见的 @Autowired、@Transactional 和 @Component 注解。

自定义注解是指开发者根据业务需求自行定义的注解,用于为代码元素添加特殊的标记或行为。Spring 中可以通过自定义注解实现 AOP、参数校验、动态配置等功能。

二、创建自定义注解

创建自定义注解包括两个主要步骤:

1.定义注解:指定注解的目标、生命周期以及参数等元信息。

2.处理注解:通过 AOP、反射或框架机制实现注解的功能。

2.1 注解的元信息

定义一个注解时,首先需要确定以下信息:

目标(Target):注解可以应用于哪些元素,如类、方法、字段、参数等。

生命周期(Retention):注解在什么阶段可见,常见的有源码级别(SOURCE)、字节码级别(CLASS)、运行时级别(RUNTIME)。

参数:注解是否需要携带参数,参数的类型和默认值是什么。

示例:定义一个简单的注解

import java.lang.annotation.*;@Target(ElementType.METHOD) // 注解可应用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时可见
public @interface LogExecutionTime {// 可选参数String value() default "";
}

2.2 处理自定义注解

创建了自定义注解后,需要通过某种机制来处理它。Spring 提供了 AOP 和反射机制,可以方便地对注解进行拦截和处理。

示例:计算方法执行时间

1. 定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
}

2. 定义切面(Aspect)

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;@Aspect
@Component
public class LoggingAspect {@Around("@annotation(LogExecutionTime)")public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {long start = System.currentTimeMillis();Object proceed = joinPoint.proceed(); // 调用目标方法long executionTime = System.currentTimeMillis() - start;System.out.println(joinPoint.getSignature() + " executed in " + executionTime + "ms");return proceed;}
}

3. 使用注解

import org.springframework.stereotype.Service;@Service
public class ExampleService {@LogExecutionTimepublic void serve() {// 模拟耗时操作try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}
}

当调用 serve() 方法时,LoggingAspect 会拦截并记录方法的执行时间。

三、自定义注解的常见用途

3.1 面向切面编程(AOP)

自定义注解在 AOP 中非常常用,用于标记需要特殊处理的方法或类,然后通过切面对其进行拦截,执行额外的逻辑。

用途

日志记录:记录方法调用、参数、返回值等。

事务管理:自定义事务处理逻辑。

异常处理:统一捕获和处理异常。

性能监控:统计方法执行时间、资源消耗等。

权限控制:验证用户权限、角色等。

示例:自定义权限控制注解

1. 定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermission {String value(); // 权限标识
}

2. 定义切面

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;@Aspect
@Component
public class SecurityAspect {@Before("@annotation(requiresPermission)")public void checkPermission(JoinPoint joinPoint, RequiresPermission requiresPermission) {String permission = requiresPermission.value();// 获取当前用户的权限boolean hasPermission = checkUserPermission(permission);if (!hasPermission) {throw new SecurityException("User does not have permission: " + permission);}}private boolean checkUserPermission(String permission) {// 具体的权限校验逻辑return true; // 简化示例,默认返回 true}
}

3. 使用注解

public class AccountService {@RequiresPermission("account:read")public void viewAccount() {// 查看账户信息}@RequiresPermission("account:write")public void updateAccount() {// 更新账户信息}
}

3.2 参数校验

自定义注解可以结合 Bean Validation(如 Hibernate Validator)实现自定义的参数校验规则。

示例:自定义手机号校验注解

1. 定义注解

import javax.validation.Constraint;
import javax.validation.Payload;@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface ValidPhoneNumber {String message() default "Invalid phone number";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};
}

2. 实现校验逻辑

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;public class PhoneNumberValidator implements ConstraintValidator<ValidPhoneNumber, String> {private static final String PHONE_PATTERN = "^\\d{10,11}$";@Overridepublic void initialize(ValidPhoneNumber constraintAnnotation) {}@Overridepublic boolean isValid(String phoneNumber, ConstraintValidatorContext context) {return phoneNumber != null && phoneNumber.matches(PHONE_PATTERN);}
}

3. 使用注解

public class User {@ValidPhoneNumberprivate String phoneNumber;// getter 和 setter
}

当使用 @Valid 注解对 User 对象进行校验时,Spring 会自动调用 PhoneNumberValidator 进行手机号格式验证。

3.3 简化配置和元注解封装

通过自定义注解,可以将多个常用注解组合在一起,减少重复代码,提高代码的可读性。

示例:自定义组合注解

1. 定义组合注解

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Service
@Transactional
public @interface TransactionalService {
}

2. 使用组合注解

@TransactionalService
public class OrderService {public void createOrder() {// 创建订单的业务逻辑}
}

这样,OrderService 类既是一个 Spring 服务,又启用了事务管理。

3.4 条件加载和配置

自定义注解可以结合 @Conditional 注解,根据特定条件决定 Bean 是否加载。

示例:根据系统属性加载 Bean

1. 定义条件注解

import org.springframework.context.annotation.Conditional;@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnSystemPropertyCondition.class)
public @interface ConditionalOnSystemProperty {String name();String value();
}

2. 实现条件逻辑

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;public class OnSystemPropertyCondition implements Condition {@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {String name = (String) metadata.getAnnotationAttributes(ConditionalOnSystemProperty.class.getName()).get("name");String value = (String) metadata.getAnnotationAttributes(ConditionalOnSystemProperty.class.getName()).get("value");String systemValue = System.getProperty(name);return value.equals(systemValue);}
}

3. 使用条件注解

@ConditionalOnSystemProperty(name = "env", value = "prod")
@Component
public class ProductionComponent {// 只有在系统属性 env=prod 时才会加载
}

3.5 动态代理和缓存

自定义注解可以用于实现动态代理机制,如缓存、事务等功能。

示例:自定义缓存注解

1. 定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cacheable {String key();
}

2. 实现缓存逻辑

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;@Aspect
@Component
public class CachingAspect {private Map<String, Object> cache = new ConcurrentHashMap<>();@Around("@annotation(cacheable)")public Object cacheMethod(ProceedingJoinPoint joinPoint, Cacheable cacheable) throws Throwable {String key = cacheable.key();if (cache.containsKey(key)) {return cache.get(key);}Object result = joinPoint.proceed();cache.put(key, result);return result;}
}

3. 使用注解

public class DataService {@Cacheable(key = "data")public String getData() {// 模拟耗时操作try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}return "Expensive Data";}
}

当第一次调用 getData() 时,会执行实际的方法逻辑;后续调用将直接从缓存中获取结果。

3.6 自动注入和数据绑定

自定义注解可以用于自动注入复杂的依赖或执行数据绑定操作。

示例:自定义配置注入注解

1. 定义注解

import org.springframework.beans.factory.annotation.Value;@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectProperty {String value();
}

2. 实现注入逻辑

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;import java.lang.reflect.Field;@Component
public class InjectPropertyPostProcessor implements BeanPostProcessor {@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {Field[] fields = bean.getClass().getDeclaredFields();for (Field field : fields) {InjectProperty annotation = field.getAnnotation(InjectProperty.class);if (annotation != null) {String value = System.getProperty(annotation.value());field.setAccessible(true);try {field.set(bean, value);} catch (IllegalAccessException e) {throw new BeansException("Failed to inject property", e) {};}}}return bean;}
}

3. 使用注解

public class ConfigurableComponent {@InjectProperty("app.name")private String appName;public void printAppName() {System.out.println("Application Name: " + appName);}
}

在运行时,appName 字段将被注入对应的系统属性值。

四、自定义注解使用注意事项

1.明确需求:在创建自定义注解前,确保理解其用途和必要性,避免过度设计。

2.保持简洁:注解的设计应当简洁明了,参数不宜过多,避免增加使用复杂度。

3.注意兼容性:确保自定义注解与 Spring 框架的版本兼容,避免使用过时或不兼容的特性。

4.文档和注释:为自定义注解添加详细的文档和注释,方便团队成员理解和使用。

5.性能考虑:在实现注解处理逻辑时,注意性能影响,避免引入不必要的开销。

五、总结

自定义注解在 Spring 开发中扮演着重要的角色,提供了高度的灵活性和可扩展性。通过合理地使用自定义注解,可以实现以下目标:

增强代码可读性:将复杂的逻辑和配置简化为易于理解的注解形式。

提高开发效率:减少样板代码,专注于业务逻辑的实现。

实现解耦和模块化:通过注解和 AOP,将横切关注点(如日志、事务、权限)与核心业务逻辑分离。

在实际开发中,应当根据具体的业务需求和项目特点,合理地设计和使用自定义注解,避免滥用。同时,结合 Spring 的强大特性,如 AOP、依赖注入、条件装配等,可以构建出更加健壮、灵活的应用程序。

希望本文能够帮助您深入理解 Spring 中自定义注解的原理和应用,为您的开发工作提供有益的参考。

参考资料

•《Spring 官方文档》

•《Java 编程思想》

•《深入理解 Java 虚拟机》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/56488.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Pyqt5设计打开电脑摄像头+可选择哪个摄像头(如有多个)

目录 专栏导读库的安装代码介绍完整代码总结 专栏导读 &#x1f338; 欢迎来到Python办公自动化专栏—Python处理办公问题&#xff0c;解放您的双手 &#x1f3f3;️‍&#x1f308; 博客主页&#xff1a;请点击——> 一晌小贪欢的博客主页求关注 &#x1f44d; 该系列文…

【C++】——list 容器的解析与极致实现

人的一切痛苦&#xff0c;本质上都是对自己的无能的愤怒。 —— 王小波 目录 1、list 介绍 2、list的使用 2.1 list 的构造 2.2 iterator 的使用 2.3 list 的修改 2.4一些特殊接口 2.5 迭代器失效问题 3、实现list 3.1底层结构 结点类 list类 迭代器类 3.2功能接…

【优选算法篇】在分割中追寻秩序:二分查找的智慧轨迹

文章目录 C 二分查找详解&#xff1a;基础题解与思维分析前言第一章&#xff1a;热身练习1.1 二分查找基本实现解题思路图解分析C代码实现易错点提示代码解读 1.2 在排序数组中查找元素的第一个和最后一个位置解题思路1.2.1 查找左边界算法步骤&#xff1a;图解分析C代码实现 1…

git clone报错fatal: pack has bad object at offset 186137397: inflate returned 1

git clone报错fatal: pack has bad object at offset 186137397: inflate returned 1 逐步拷贝 https://stackoverflow.com/questions/27653116/git-fatal-pack-has-bad-object-at-offset-x-inflate-returned-5 https://www.cnblogs.com/Lenbrother/p/17726195.html https://…

在UE引擎中使用spine动画(1)

注意事项&#xff0c;spine的版本必须和UE插件的版本相同。 1.最重要的是“修改骨架名称。&#xff08;影响在UE引擎中的资产名称&#xff09; 2.导出操作&#xff08;把非必要的数据取消掉&#xff0c;可能会影响UE导入&#xff09;。 3.纹理打包&#xff08;一般默认&#…

程序员:数字时代的先锋

随着科技的不断进步&#xff0c;程序员这一职业群体逐渐成为社会中不可或缺的一部分。他们以智慧和汗水为世界带来更多的便捷与创新。今天&#xff0c;我们将庆祝1024程序员节&#xff0c;这是一个向全球程序员们表达敬意和感激的节日。让我们一同走进程序员的内心世界&#xf…

Vue学习笔记(八、v-model双向绑定)

与v-bind只能实现单向绑定不同&#xff0c;v-model可以实现数据的双向绑定&#xff0c;不过v-model只能用于表单元素中&#xff0c;例如&#xff1a;input(radio,text,address,email...) 、select、checkbox、textarea。 下面代码是一个v-model双向绑定的演示&#xff1a; <…

数字分组求偶数和

问题描述 小M面对一组从 1 到 9 的数字&#xff0c;这些数字被分成多个小组&#xff0c;并从每个小组中选择一个数字组成一个新的数。目标是使得这个新数的各位数字之和为偶数。任务是计算出有多少种不同的分组和选择方法可以达到这一目标。 numbers: 一个由多个整数字符串组…

[ 钓鱼实战系列-基础篇-7 ] 一篇文章教会你搭建邮件钓鱼服务器-1

&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 _PowerShell &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 &#x1f389;点赞➕评论➕收藏 养成习…

js.杨辉三角和分发饼干

1&#xff0c;链接&#xff1a;118. 杨辉三角 - 力扣&#xff08;LeetCode&#xff09; 题目&#xff1a; 给定一个非负整数 numRows&#xff0c;生成「杨辉三角」的前 numRows 行。 在「杨辉三角」中&#xff0c;每个数是它左上方和右上方的数的和。 示例 1: 输入: numRows …

代码随想录算法训练营第二十五天|Day25 回溯算法

491.递增子序列 https://programmercarl.com/0491.%E9%80%92%E5%A2%9E%E5%AD%90%E5%BA%8F%E5%88%97.html 视频讲解&#xff1a;https://www.bilibili.com/video/BV1EG4y1h78v 思路 int* path; int pathTop; int** ans; int ansTop; int* length; void copy() {int* tempPath …

动态规划-子序列问题——300.最长递增子序列

1.题目解析 题目来源&#xff1a;300.最长递增子序列——力扣 测试用例 2.算法原理 1.状态表示 首先创建一个与数组大小相同的dp表&#xff0c;此时dp[i]表示的是&#xff1a;以第i个位置为结尾的所有子序列中最长递增子序列的长度 2.状态转移方程 此时第i个位置一定是最长递…

国家能源集团携手海康威视研发攻克融合光谱煤质快检技术

10月24日&#xff0c;在国家能源集团准能集团黑岱沟露天煤矿&#xff0c;安装于准能选煤厂785商品煤胶带机中部的煤质快检核心设备&#xff0c;正在对当天装车外运的商品煤煤质进行实时检测。仅两分钟后&#xff0c;涵盖发热量、水分、灰分、硫分等多项指标的数据信息已传输到到…

六.python面向对象

学过C或者Java的同学一定了解过面向对象的相关内容&#xff0c;编程语言一般分为两种设计方式&#xff1a;面向对象、面向过程&#xff0c;早期的编程语言多是面向过程的&#xff0c;由多个过程组合在一起&#xff0c;而Python在设计的时候就是一种面向对象的语言&#xff0c;因…

Room新手入门

Room Room 是所谓的 ORM&#xff08;对象关系映射&#xff09;库 依赖 在项目级 build.gradle 文件中&#xff0c;在 ext 代码块中定义 room_version。 ext {kotlin_version "1.6.20"nav_version "2.4.1"room_version 2.4.2 }在应用级 build.gradle…

【JavaEE初阶】深入理解TCP协议中的封装分用以及UDP和TCP在网络编程的区别

前言 &#x1f31f;&#x1f31f;本期讲解关于TCP/UDP协议的原理理解~~~ &#x1f308;上期博客在这里&#xff1a;【JavaEE初阶】入门视角-网络原理的基础理论的了解-CSDN博客 &#x1f308;感兴趣的小伙伴看一看小编主页&#xff1a;GGBondlctrl-CSDN博客 &#x1f525; …

【Spring】关于Spring中aware相关接口的作用

Aware 接口的回调方法是在 Bean 实例化之后调用的。具体来说&#xff0c;这些回调方法是在依赖注入完成后&#xff0c;但在 Bean 完全初始化之前调用的。这是 Spring 容器管理 Bean 生命周期的一部分 完成了属性赋值之后&#xff0c;Spring会执行一些回调&#xff0c;包括&…

java中Set,Map,List集合的比较(不包含增删改查函数方法)

目录 1. 集合的简介2. List3. Set4. Map5. 比较5.1 结构特点5.2 实现类5.3 区别 6. 其他问题6.1 集合与数组的区别6.2 哪些集合类是线程安全的 7. 参考链接 1. 集合的简介 所有的集合类和集合接口都在java.util包下。 在内存中申请一块空间用来存储数据&#xff0c;在Java中集…

langchain的使用以及算力的计算

文章目录 提示词提示词 import warnings warnings.filterwarnings("ignore") from langchain import PromptTemplate,LLMChain from llm import ChatGLM# 简单的问题模板 template = """问题: {question}答案: """ prompt = PromptTe…

C++加载sqlite3数据库文件

db数据库文件简单轻便&#xff0c;形式上可以像excel数据那样&#xff0c;但是解析的时候却有很大的灵活性。使用Python解析db数据已经是很简单的事情&#xff0c;使用专门的数据库可视化工具&#xff0c;通过sql语句查询内容也是有手就行&#xff0c;也许C也不难&#xff0c;但…