文章目录
简介
在spring项目中使用注解,简化了代码量,减轻对业务代码的侵入性;对框架统一处理鉴权、日志等起到极大的作用,可以结合着拦截器、aop在请求调用前后添加额外处理。spring有内置的@Controller、@Service等注解,出于业务考虑,我们可以自定义想要的注解。
一、定义注解
自定义注解类似于定义接口,但是需要指明注解的作用范围、生命周期等属性。
1.注解示例
下面是一个简单的自定义注解示例,使用@interface修饰,定义了三个属性值,使用注解的时候可以给这些属性赋值。
@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic@interfaceLogAnnotation{StringmoduleName()default"";StringoperaName()default"";StringoperaType()default"";}
2.元注解含义
从jdk1.5开始,在包java.lang.annotation下提供了四种元注解:@Target、@Retention、@Documented、@Inherited,java1.8后,annotation包下新提供了两种元注解:@Native、@Repeatable。自定义注解的时候需要使用元注解修饰,来看下各个元注解的使用说明。
(1)@Target
标识注解可以使用的范围,例如使用在方法、字段、构造方法上。看下源码:
//Target源码@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.ANNOTATION_TYPE)public@interfaceTarget{ElementType[]value();}//Target可配置的类型publicenumElementType{
TYPE,
FIELD,
METHOD,
PARAMETER,
CONSTRUCTOR,
LOCAL_VARIABLE,
ANNOTATION_TYPE,
PACKAGE,
TYPE_PARAMETER,
TYPE_USE
}
从源码中可以看出@Target只有一个属性value,属性类型为ElementType类型的数组,ElementType各个枚举值的作用范围如下:
①ElementType.TYPE:允许被修饰的注解作用在:类、接口、枚举上;
②ElementType.FIELD:允许被修饰的注解作用在:属性字段上;
③ElementType.METHOD:允许被修饰的注解作用在:方法上;
④ElementType.PARAMETER:允许被修饰的注解作用在:方法参数上;
⑤ElementType.CONSTRUCTOR:允许被修饰的注解作用在:构造器上;
⑥ElementType.LOCAL_VARIABLE:允许被修饰的注解作用在:本地局部变量上;
⑦ElementType.ANNOTATION_TYPE:允许被修饰的注解作用在:注解上;
⑧ElementType.PACKAGE:允许被修饰的注解作用在:包名上;
⑨ElementType.TYPE_PARAMETER:允许被修饰的注解作用在:类型参数上,jdk1.8提供;
//ElementType.TYPE_PARAMETER示例@Target(ElementType.TYPE_PARAMETER)@Retention(RetentionPolicy.RUNTIME)public@interfaceTypeParameterAnnotation{}//泛型声明publicclassTypeParameterClass<@TypeParameterAnnotationT>{public<@TypeParameterAnnotationP>Ttoo(T t){return t;}}
⑩ElementType.TYPE_USE:允许被修饰的注解作用在:任何语句中(声明语句、泛型、强制转化),jdk1.8提供。
(2)@Retention
标识注解的生命周期,来看下源码:
//Retention源码@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.ANNOTATION_TYPE)public@interfaceRetention{RetentionPolicyvalue();}//RetentionPolicy源码publicenumRetentionPolicy{
SOURCE,
CLASS,
RUNTIME
}
从源码可以看出@Retention只有一个属性value,属性类型为RetentionPolicy,看下RetentionPolicy枚举值的生命周期:
①RetentionPolicy.SOURCE:编译阶段丢弃,编译之后注解没有任何作用,不会写入字节码文件中。例如@Override、@SuppressWarnings、@Deprecated都属于这类注解;
②RetentionPolicy.CLASS:类加载阶段丢弃,类加载进jvm后没有任何作用,在字节码文件处理中有用。注解默认使用这种方式;
③RetentionPolicy.RUNTIME:始终不会丢弃,程序运行期也保留此注解,自定义注解通常使用这种方式,因此可以通过反射获取到注解配置的属性值。
(3)@Documented
标识注解是否在javadoc文档中显示,看下源码:
@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.ANNOTATION_TYPE)public@interfaceDocumented{}
当定义的注解中加入了@Documented元注解,则生成的javadoc文档中包含注解,来看一个例子:
@Documentedpublic@interfaceDocumentAnnotation{Stringname()default"张三";intage()default18;}publicclassDocumentTest{@DocumentAnnotation(name="lisi",age =30)publicvoidtest(){}}
此时生成javadoc文件,生成的方式为:
文档中包含注解信息:
自定义注解DocumentAnnotation去掉@Documented,javadoc文档中不包含注解:
(4)@Inherited
标识注解是否能继承到子类,看下源码:
@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.ANNOTATION_TYPE)public@interfaceInherited{}
使用@Inherited修饰的注解,在class使用它时,class的子类能够继承此注解,类似于InheritableThreadLocal,父子类能够共享资源。
(5)@Native
标识字段是否可以被本地代码引用,看下源码:
@Documented@Target(ElementType.FIELD)@Retention(RetentionPolicy.SOURCE)public@interfaceNative{}
此注解作用在字段上,生命周期为编译阶段丢弃。
(6)@Repeatable
标识可以重复使用注解,看下源码:
@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.ANNOTATION_TYPE)public@interfaceRepeatable{Class<?extendsAnnotation>value();}
作用在注解上,只有一个属性value,属性的类型继承了Annotation,之所以继承Annotation是因为Annotation是所有注解的父接口,看下关系图:
来看一个demo:
//定义注解@Target(value={ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceRepeatableAnnotations{RepeatableAnnotation[]value();}//定义注解,Repeatable声明RepeatableAnnotations@Target(value={ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)@Documented@Repeatable(RepeatableAnnotations.class)public@interfaceRepeatableAnnotation{Stringname();intage();}//测试类publicclassRepeatableDemo{@RepeatableAnnotation(name="张三",age=18)@RepeatableAnnotation(name="李四",age=30)privateString userMessage;publicstaticvoidmain(String[] args)throwsNoSuchFieldException{Field declaredField =RepeatableDemo.class.getDeclaredField("userMessage");Annotation[] annotations = declaredField.getDeclaredAnnotations();System.out.println("注解的数量:"+annotations.length);System.out.println("注解内容:"+Arrays.toString(annotations));}}
测试类输出结果:
注解的数量:1
注解内容:[@com.RepeatableAnnotations(value=[@com.RepeatableAnnotation(name=张三, age=18), @com.RepeatableAnnotation(name=李四, age=30)])]
定义一个可重复的注解,需要使用@Repeatable来声明,@Repeatable的值为此原注解数组形式的新注解。从测试类可以看出最终注解的数量还是1个,是使用@Repeatable值的数组形式接收,每个值为原注解类型。
在spring中ComponentScan定义bean的扫描范围,就是这样使用的,看下它的源码:
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE})@Documented@Repeatable(ComponentScans.class)public@interfaceComponentScan{}@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE})@Documentedpublic@interfaceComponentScans{ComponentScan[]value();}//使用@ComponentScan(basePackages ={"com.xxx1","com.xxx2"})
使用@Repeatable注意事项:
①原注解的@Target作用范围要比@Repeatable值的范围大或者相同,否则编译错误,例如:
//比RepeatableAnnotation多了ElementType.METHOD@Target(value={ElementType.FIELD,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceRepeatableAnnotations{RepeatableAnnotation[]value();}@Target(value={ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)@Documented@Repeatable(RepeatableAnnotations.class)public@interfaceRepeatableAnnotation{Stringname();intage();}
②原注解的@Retention生命周期要比@Repeatable值的小或者相同,否则编译错误,生命周期大小:SOURCE <
CLASS < RUNTIME。例如:
//定义的CLASS比RUNTIME要小@Target(value={ElementType.FIELD})@Retention(RetentionPolicy.CLASS)@Documentedpublic@interfaceRepeatableAnnotations{RepeatableAnnotation[]value();}@Target(value={ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)@Documented@Repeatable(RepeatableAnnotations.class)public@interfaceRepeatableAnnotation{Stringname();intage();}
二、使用注解
定义注解就是为了方便系统开发,现在来看一些使用场景。
1.aop切点使用注解
自定义注解结合着aop来使用的场景很多,例如日志的收集就可以使用。
①定义注解:
@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceLogAnnotation{//模块名称(枚举类)ModuleNameEnummoduleName()defaultModuleNameEnum.UNKNOWN;//操作对象StringoperaName()default"";//操作类型(枚举类)OperaTypeEnumoperaType()defaultOperaTypeEnum.UNKNOWN;}
②定义aop切面类:
@Aspect@Component@Slf4jpublicclassLogAspect{@AutowiredXxxLogService xxxLogService;//切点:使用LogAnnotation注解标识的方法都进行切入,也可以使用通配符配置具体要切入的方法名@Pointcut("@annotation(com.xxx.aop.LogAnnotation)")publicvoidpointCut(){}//环绕通知@Around("pointCut()")publicObjectlogAround(ProceedingJoinPoint joinPoint)throwsThrowable{Object jsonResult = joinPoint.proceed();//执行方法try{//获取请求签名MethodSignature signature =(MethodSignature)joinPoint.getSignature();//获取切入点所在的方法Method method = signature.getMethod();//获取注解值LogAnnotation annotation = method.getAnnotation(LogAnnotation.class);//获取属性String moduleName = annotation.moduleName().getValue();String operaName = annotation.operaName();String operaType = annotation.operaType().getValue();XxxLog xxxLog =newXxxLog();
xxxLog.setModuleName(moduleName);
xxxLog.setOperaName(operaName);
xxxLog.setOperaType(operaType);//添加日志
xxxLogService.insertOne(xxxLog);}catch(Exception e){
e.printStackTrace();}catch(Throwable e){
e.printStackTrace();}return jsonResult;}}
③方法中添加注解
当注解属性名为value时,赋值的时候可以省略属性名,其他名称的属性名需要使用xx=yy的方式指定。
@LogAnnotation(moduleName=ModuleNameEnum.FeedBack,operaName="添加消息",operaType=OperaTypeEnum.Insert)publicvoidinsertOne(Integer id){}
过程为:定义注解,定义属性值;创建切面类,使用@annotation来指定切点为自定义注解,环绕方法获取注解及属性值,把属性值保存到业务数据库中;业务代码中需要保存日志的方法加上注解,并设置属性值。
2.拦截器获取注解
可以在拦截器中获取注解,在controller层响应前后做一些额外的处理或判断,例如判断权限、判断是否需要分页等。来看一个分页的demo:
①定义注解
@Target(ElementType.METHOD)@Documented@Retention(RetentionPolicy.RUNTIME)public@interfaceEnablePaging{intvalue()default50;}
②定义拦截器
publicclassPagingInterceptorimplementsHandlerInterceptor{//controller响应之前执行@OverridepublicbooleanpreHandle(@NotNullHttpServletRequest request,@NotNullHttpServletResponse response,@NotNullObject handler){if(!(handler instanceofHandlerMethod)){returntrue;}HandlerMethod handlerMethod =(HandlerMethod) handler;//获取方法中的注解EnablePaging enablePaging = handlerMethod.getMethodAnnotation(EnablePaging.class);//不包含注解,直接通过if(enablePaging ==null){returntrue;}//包含注解,则获取注解中的值,值保存到TreadLocal线程变量中(此处使用RequestContextHolder.currentRequestAttributes().setAttribute保存),在执行sql查询时取出使用PagingContextData data =PagingContextData.getInstance(RequestAttributes.SCOPE_REQUEST,true);
data.setEnabled(true);//把注解中配置的值设置进去if(enablePaging.value()>0){
data.setDefaultPageSize(enablePaging.value());}returntrue;}}
③注册拦截器
@ConfigurationpublicclassPagingHttpConfigimplementsWebMvcConfigurer{@OverridepublicvoidaddInterceptors(InterceptorRegistry registry){
registry.addInterceptor(newPagingInterceptor()).order(Ordered.HIGHEST_PRECEDENCE);}}
④方法中添加注解
@PostMapping("/datasource/xxxPage")@EnablePaging(20)publicObjectxxxPage(@RequestBodyString json){return xxxService.xxxPage(json);}
过程为:定义注解,定义属性值;创建拦截器,在拦截器的方法中获取注解及属性值,把属性值保存到线程变量ThreadLocal中;把拦截器注册到InterceptorRegistry中;业务代码中需要分页的接口方法加上注解,并设置属性值。
3.class获取注解
通过class可以获取到注解,提供了从method方法、field字段等获取注解。获取class的方式有:
①对象.getClass()方法
Student stu =newStudent();Class clazz = stu.getClass();
②对象.class
Class clazz =Student.class;
③Class.forName(“xxx”),例如数据库驱动的获取
Class clazz =Class.forName("com.xxx.Student")
从method中获取注解示例:
//获取所有方法Method[] methods =SampleClass.class.getMethods();for(int i =0;i < methods.length;i++){//获取方法中的注解CustomAnnotaion annotation = methods[i].getAnnotation(CustomAnnotaion.class);if(null!= annotation){//输出属性值System.out.println(annotation.name());}}//获取指定方法Method oneMethod =SampleClass.class.getDeclaredMethod("getSampleField");//获取方法中的注解值CustomAnnotaion annotation = oneMethod.getAnnotation(CustomAnnotaion.class);System.out.println("annotation="+annotation.name());
从字段中获取注解示例:
//获取指定字段Field declaredField =RepeatableDemo.class.getDeclaredField("userMessage");//获取字段中的注解Annotation[] annotations = declaredField.getDeclaredAnnotations();
4.spring容器获取注解
在bean对象中加入注解,当spring容器加载完bean之后,可以从bean中获取到哪些方法加了指定的注解,从而拿到方法,对这些方法进行特殊处理。在xxl-job开源项目中就有使用,看下使用方式:
privatevoidinitJobHandlerMethodRepository(ApplicationContext applicationContext){if(applicationContext ==null){return;}// init job handler from method//从程序上下文中获取到所有的bean名称集合String[] beanDefinitionNames = applicationContext.getBeanNamesForType(Object.class,false,true);//遍历bean集合for(String beanDefinitionName : beanDefinitionNames){//根据bean名称从程序上下文获取到此bean对象Object bean = applicationContext.getBean(beanDefinitionName);Map<Method,XxlJob> annotatedMethods =null;try{//对Bean对象进行方法过滤,查询到方法被XxlJob注解修饰,是则放到annotatedMethods集合中
annotatedMethods =MethodIntrospector.selectMethods(bean.getClass(),newMethodIntrospector.MetadataLookup<XxlJob>(){@OverridepublicXxlJobinspect(Method method){//判断方法被XxlJob注解修饰才返回returnAnnotatedElementUtils.findMergedAnnotation(method,XxlJob.class);}});}catch(Throwable ex){
logger.error("xxl-job method-jobhandler resolve error for bean["+ beanDefinitionName +"].", ex);}//当前遍历的bean没有被XxlJob注解修饰,则跳过处理if(annotatedMethods==null|| annotatedMethods.isEmpty()){continue;}//循环处理当前Bean下被XxlJob修饰的方法for(Map.Entry<Method,XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()){//执行的方法Method executeMethod = methodXxlJobEntry.getKey();//XxlJob注解类XxlJob xxlJob = methodXxlJobEntry.getValue();//注册此任务处理器registJobHandler(xxlJob, bean, executeMethod);}}}
从spring上下文applicationContext中获取到所有的bean名称集合,遍历bean名称集合,根据bean名称从程序上下文获取到此bean对象,对Bean对象进行方法过滤,查询到被XxlJob注解修饰的方法,放到map集合中,循环处理map中的记录,key为Method方法,value为XxlJob注解,这也是使用注解的场景。
版权归原作者 清云青云 所有, 如有侵权,请联系我们删除。