在大家使用spring MVC或Hibernate 3.0以上的版本时,可能会注意到annotation带来的方便性,不过这往往让人觉得annotation真的很强大,而这算是一种接近错误的理解吧,annotation其实本身是属于一种文档注解的方式,帮助我们在编译时、运行时、文档生成时使用,部分annotation其实基本和注释差不多,这里其实是要说下annotation的原理,以及各种功能在它上面如何实现的,以及在继承的时候,他会发生什么?为什么会这样?
首先,就我个人使用的理解,annotation是一种在类、类型、属性、参数、局部变量、方法、构造方法、包、annotation本身等上面的一个附属品(ElementType这个枚举中有阐述),他依赖于这些元素而存在,他本身并没有任何作用,annotation的作用是根据其附属在这些对象上,根据外部程序解析引起了他的作用,例如编译时的,其实编译阶段就在运行:java Compiler,他就会检查这些元素,例如:@SuppressWarnings、@Override、@Deprecated等等;
生成文档运行javadoc也是单独的一个进程去解析的,其实他是识别这些内容的,而spring MVC和Hibernate的注解,框架程序在运行时去解析这些annotation,至于运行的初始化还是什么时候要和具体的框架结合起来看,那么今天我们就要说下所谓的annotation是如何实现功能的(再次强调:它本身没有功能,功能又程序决定,他只是上面描述的几大元素的附属品而已,如果认为他本身有功能,就永远不知道annotation是什么);
我们首先自己来写个annotation,写annotation就像写类一样,创建一个java文件,和annotation的名称保持一致,他也会生成class文件,说明他也是java,只是以前要么是interface、abstract class、class开头,现在多了一个@interface,可见它是属于jvm可以识别的一种新的对象,就像序列化接口一样的标记,那么我们简单写一个:
下面的代码可能你看了觉得没啥意思,接着向下可能你会找到有意思的地方:
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, ElementType.CONSTRUCTOR , ElementType.FIELD , ElementType.TYPE}) public @interface NewAnnotation { String value() default ""; }
那么上面的annotation代表:
在Runtime的时候将会使用(RetentionPolicy里面有阐述其他的SOURCE、CLASS级别),可以注解到方法、构造方法、属性、类型和类上面;名称为:NewAnnotation、里面有一个属性为value,为String类型,默认值为空字符串,也就是可以不传递参数。
程序中使用例如:
public class A { @NewAnnotation private String b; @NewAnnotation(value = "abc") public void setB() {...} }
那么很多人看到这里都会问,这样写了有什么用途呢?貌似是没啥用途,我第一次看到这里也没太看懂,而且看到spring MVC做得如此多功能,这到底是怎么回事?
再一些项目的框架制作中,我逐步发现一些功能,如果有一种代码的附属品,将会将框架制作得更加漂亮和简洁,于是又联想到了spring的东西,spring的AOP是基于字节码增强技术完成,拦截器的实现不再是神话,那么反过来如如果annotation是可以被解析的,基于annotation的注入就是十分简单明了的事情了,Hibernate也是如此,当然我这不讨论一些解析的缓存问题,因为不会让每个对象都这样去解析一次,都会尽量记忆下来使得性能更高,这里只说他的原理而已。
这里拿一个简单的request对象转换为javaBean对象的,假如我们用DO为后缀,而部分请求的参数名和实际的属性并不一样(一般规范要求是一样的),其次在网络传输中某个项目前台的日期提交到后台都是以毫秒之方式提交到后台,但是需要转换为对应的字符串格式来处理,提交中包含:String、int、Integer、Long、long、String[]这几种数据类型,日期的我们大家可以扩展,带着这些小需求我们来简单写一个:
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.FIELD}) public @interface ReuqestAnnotation { String name() default "";//传入的参数名 boolean dateString() default false;//是否为dateString类型 }
这里的annotation一个是name一个是dateString,两个都有默认值,name我们认为是request中参数名,否则直接以属性名为主,dateString属性是否是日期字符串(前面描述传递的日期都会变成毫秒值,所以需要自动转换下)。
那么我们写一个DO:
import xxx.xxx.ReuqestAnnotation;//import部分请自己根据项目引用 public class RequestTemplateDO { private String name; @ReuqestAnnotation(name = "myemail") private String email; private String desc; @ReuqestAnnotation(dateString = true) private String inputDate; private Integer int1; private int int2; public int getInt1() { return int1; } public int getInt2() { return int2; } public String getInputDate() { return inputDate; } public String getName() { return name; } public String getEmail() { return email; } public String getDesc() { return desc; } }
注意这里没有写set方法,我们为了说明问题,而不是怎么去调用set方法,所以我们直接用属性去设置值,属性是private一样可以设置,下面就是一个转换方法:
假如有一个HttpUtils,我们写一个静态方法:
/** * 通过request获取对应的对象 * @param <T> * @param request * @param clazz * @return * @throws IllegalAccessException * @throws InstantiationException */ @SuppressWarnings("unchecked") public static <T extends Object> T convertRequestToDO(HttpServletRequest request , Class<T> clazz) throws InstantiationException, IllegalAccessException { Object object = clazz.newInstance(); Field []fields = clazz.getDeclaredFields(); for(Field field : fields) { field.setAccessible(true); String requestName = field.getName(); ReuqestAnnotation requestAnnotation = field.getAnnotation(ReuqestAnnotation.class); boolean isDateString = false; if(requestAnnotation != null) { if(StringUtils.isNotEmpty(requestAnnotation.name())) requestName = requestAnnotation.name(); isDateString = requestAnnotation.dateString(); } Class <?>clazzf = field.getType(); if(clazzf == String.class) { if(isDateString) { String dateStr = request.getParameter(requestName); if(dateStr != null) { field.set(object, DateTimeUtil.getDateTime(new Date(Long.valueOf(dateStr)) , DateTimeUtil.DEFAULT_DATE_FORMAT)); } }else { field.set(object, request.getParameter(requestName)); } } else if(clazzf == Integer.class) field.set(object, getInteger(request.getParameter(requestName))); else if(clazzf == int.class) field.set(object, getInt(request.getParameter(requestName))); else if(clazzf == Long.class) field.set(object, getLongWapper(request.getParameter(requestName))); else if(clazzf == long.class) field.setLong(object, getLong(request.getParameter(requestName))); else if(clazzf == String[].class) field.set(object, request.getParameterValues(requestName)); } return (T)object; }
这里面就会负责将request相应的值填充到数据中,返回对应的DO,而代码中使用的是:
RequestTemplateDO requestTemplateDO = HttpUtils.convertRequestToDO(request , RequestTemplateDO.class);
注意:这部分spring帮我们写了,只是我在说大概原理,而且spring本身实现和这部分也有区别,也更加完整,这里仅仅是为了说明局部问题。spring在拦截器中拦截后就可以组装好这个DO,所以在spring MVC中可以将其直接作为扩展参数传递进入我们的业务方法中,首先知道业务方法的annotation,根据URL决定方法后,获取参数列表,根据参数类型,如果是业务DO,那么填充业务DO即可,Hibernate也可以同样的方式去推理。
OK,貌似很简单,如果你真的觉得简单了,那么这块你就真的懂了,那么我们说点特殊的,就是继承,貌似annotation很少去继承,但是在我遇到一些朋友的项目中,由于部分设计需要或本身设计缺陷但是又不想修改的时候,就会遇到,多个DO大部分属性是一样的,如果不抽象父亲类出来,如果修改属性要同时修改非常多的DO,而且操作的时候绝大部分情况是操作这些共享的属性,所以还想用上溯造型来完成代码的通用性并保持多态,当时一问我还真蒙了,因为是基于类似annotation的一些框架,例如hibernate,后来带着问题做了很多测试并且和资料对应上,是如果annotation在class级别、构造方法级别,是不会被子类所拥有的,也就是当子类通过XXX.class.getAnnotation(XXXAnnotation.class)的时候是获取不到,不过public类型的方法、public的属性是可以的,其次,如果子类重写了父类的某个属性或某个方法,不管子类是否写过annotation,这个子类中父类的属性或方法的所有的annotation全部失效,也就是如父亲类有一个属性A,有两个annotation,若A是public的,子类可以继承这个属性和annotation,若子类也有一个A属性,不管A是否有annotation,父类中这些annotation在子类中都将失效掉。
这就是为什么我说annotation是属性、方法、包。。。的附属品,他是被绑定在这些元素上的,而并非拥有实际功能,当重写的时候,将会被覆盖,属性、方法当被覆盖的时候,其annotation也随之被覆盖,而不会按照annotation再单独有一个覆盖;所以当时要解决那个问题,我就告诉他,要用的方法只有public,否则没办法,即使用反射也不行,因为hibernate根本找不到这个field,还没有机会提供一个setAccessible的能力,因为这个时候根本看不到这些field,但是通过父类本身可以看到这些field;就技术层面是这样的,否则只有修改设计是最佳的方法。