Java必知:注解(注解方式实现SpringIoc的思路)
什么是注解
注解和注释的区别:
注释(comment): 单词直译是”评论,解释”,在Java的编写过程中我们需要对一些程序进行说明,除了自己方便阅读,更为别人更好理解自己的程序。
注解(annotation): 单词直译是”注文,评注”,最为主要的点在于”解”。即程序在运行过程中,需要按照规范来进行解释它,甚至将会影响程序的运行逻辑。
注释与注解最主要的区别是注释不参与编译和运行。只是文字的说明而已,注解的话参与代码的解释和运行过程。
注解的详细定义:
修饰java程序元素的元信息,java语言的元素包括类,方法,变量,参数,包。注解的核心作用就是修饰这些元素,甚至在编译或者运行的过程中,影响这些元素。
注解产生的背景:
开发时间长点的朋友肯定配置过Spring或者Struts的XML配置。在以往我们是基于XML进行相关的配置,但是如果你总结过的话,整个XML万变不离其宗的是,他的配置都是对java的元素进行配置。比如配置类的包类名全路径,接口名,方法名,字段名等等…但是大家会发现随着业务的复杂度和项目不变的变大。XML的内容也越来越复杂,而且维护成本很高.因为本身代码和XML的配置之间并没有那么直观的产生联系,后续加入团队的成员需要从头理顺这个配置文件的含义和表达,成本是很高的。所以有人提出来,是否有一种标记式的高耦合的配置方法来解决这样得问题。所以就产生了“注解”,注解是JAVA5.0推出来的新特性。
注解的优势:
下面通过代码,我们来实际的看看注解给我们的开发带来的便利。
例如:spring中bean实例化:
XML的方式:
Annotation的方式:
通过上诉的案例,我们通过xml和annotation的方式都可以完成bean实例对象的创建过程,但是annotation在代码的简洁度和可读性上具有非常大的优势。所以注解目前广泛的运用于我们的各大框架技术。后续我们想阅读源码理解大牛的设计思想,注解是非常常见的内容,所以学好注解也是一个必然的趋势。
注解的学习
注解的本质理解:
注解从代码层面来看,其实就是一个接口,一个继承了Annotation的接口。
注解的架构:
从上图,我们可以得知:ElementType
:代表当前注解用于那种元素类型上(比如,类,方法等…)。Annotation与ElementType是1:N的关系RetentionPolicy:
代表当前注解的有效期范围。Annotation与RetentionPolicy是1:1的关系Deprecated,Override,Documented...:
JDK自带的一些内置的注解实现
接下来我们通过学习JDK中内置的注解的方式,来体系的了解注解。
内置注解:
Deprecated:
这个注解是用来标记已经过时(即不被建议使用)的元素[方法.字段.类…]
例如:
使用了这样注解的元素,将会出现这样的标记,这个标记即表示这个构造器是一个过时的构造器,不建议再使用。
Override:
这个注解相信大家很熟悉吧。这个注解是用来标记方法的重写。
通过@Override注解我们可以很快速的知道当前方法是覆写自接口或者父类。
通过上述案例.我们知道了@Deprecated这个注解是用来标记已经过时(即不被建议使用)的元素[方法.字段.类…]
通过@Override注解我们可以很快速的知道当前方法是覆写自接口或者父类.
那@Deprecated到底能作用于哪些元素,@Override能作用于哪些元素?接下来我们来讲解JDK中定义的元注解
。
元注解:
概念:注解的注解且只能作用于注解上的注解就是元注解。????有点绕不过不难理解。
@Target
定义该注解可以用于哪些元素。取值范围ElementType枚举,可以多个取值。也就是上边说到的1:N的关系
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 //使用类型
}
@Retention
定义注解的保留策略,或者说定义注解的有效范围。取值范围RetationPolicy枚举
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 //注解的作用存在于源码阶段,字节码和运行期阶段,且注解的内容会被JVM执行
}
这里的三个枚举值是有等级关系的:
Source < Class < Runtime 也可以这么理解Runtime的有效期范围是最大的.其次是Class,最小的即是Source
注意:
1.在运行期间通过反射的方式去获取元素的注解,那就只能是RetetionPolicy.RUNTIME
2.如果要在编译时进行一些预处理操作,我们可以将我们级别定位成RetetionPolicy.CLASS
3.如果是一些检查性的工作,或者生成辅助的代码等等。比如@Override 就是检查我们的方法必须是覆写的方法,或者lombok 中@Data @Setter @Getter注解
@Documented:
标记使用的注解是否包含在生成的用户文档中。@Inherited:
如果一个使用了@Inherited修饰的annotation类型被用于一个类,则这个annotation将被用于该class的子类。仅限父类使用@Inherited标记的注解,子类将自动获得父类的该注解,接口的继承和接口的实现都不继承。
自定义注解(注解方式实现SpringIoc的思路)
接下来我们还是基于Spring的IOC ,我们采用模仿 Component 和 Bean 两个注解的方式进行自定义注解的学习。
我们定义了2个注解分别为CzyComponent 和 CzyBean
我们指定 @CzyComponent 注解,使用元素 ElementType.TYPE指定作用于类上
有效期范围在运行期都有效 RetentionPolicy.RUNTIME ,因为我们要基于反射进行bean实例的创建,反射的定义我们也了解过,在运行期间的一套机制,所以指定运行期有效。
我们指定 @CzyBean 注解,使用元素 ElementType.Method指定作用于方法上,有效期范围在运行期都有效 RetentionPolicy.RUNTIME 因为我们要基于反射进行bean实例的创建,反射的定义我们也了解过,在运行期间的一套机制,所以指定运行期有效。
class A,class B,class C :
ioc容器:
public class IOCContainer {
private static HashMap container = new HashMap();
public static void putBean(String id,Object object){
container.put(id,object);
}
public static Object getBean(String id){
return container.get(id);
}
}
接下来基于这样的配置,我们进行注解方式的IOC实现:
第一步进行Class文件的扫描:
说明:我们在xml中肯定配置的有这样的包扫描标签,spring中肯定还是先读取配置文件,获取出来要扫描的包路径即com.czy.project.demo2
下的类,这里我们就先省略读取xml获取包路径的步骤,直接进行class扫描的步骤。
编写进行Class文件的扫描的方法:
/**
* 得到指定包下所有的class 的全路径
* @param allClassPathSet:装载类全路径的集合
* @param scanPackage:指定扫描的包路径
* 注意:该方法为Main类中的静态方法
*/
public static void doScanner(Set<String> allClassPathSet, String scanPackage) {
URL url = Main.class.getResource("/" + scanPackage.replaceAll("\\.", "/"));
File classDir = new File(url.getFile());
for (File file : classDir.listFiles()) {
if (file.isDirectory()) {
//是文件夹,递归循环
doScanner(allClassPathSet, scanPackage + "." + file.getName());
} else {
//如果文件不是.class结尾
if (!file.getName().endsWith(".class")) {
continue;
}
//拼装类的全路径
String clazzName = (scanPackage + "." + file.getName().replace(".class", ""));
//添加到集合中
allClassPathSet.add(clazzName);
}
}
}
第二步遍历Class文件通过反射判断注解信息,通过反射完成Bean的实例化过程,放入IOC容器中
for (String className : allClassPathSet) {
Class<?> clazz = Class.forName(className);//获取该class实例
//如果该类上有@CzyComponent注解,就直接将该类添加到ioc容器中
if (clazz.isAnnotationPresent(CzyComponent.class)) {
IOCContainer.putBean(className, clazz.newInstance());
}
//获取该该类中的所有方法
Method[] methods = clazz.getDeclaredMethods();
for (int i = 0; i < methods.length; i++) {
Method method = methods[i];
if (method.isAnnotationPresent(CzyBean.class)) { //如果该方法上有@CzyBean注解
//获取beanName例如@CzyBean("b")即获取出;b
String beanName = method.getAnnotation(CzyBean.class).value();
//判断该方法是否为静态方法
if (Modifier.isStatic(method.getModifiers())) {
IOCContainer.putBean(beanName, method.invoke(null));//静态工厂创建实例
} else {
//从容器中获取当前这实例对象
//实例工厂创建实例
IOCContainer.putBean(beanName, method.invoke(IOCContainer.getBean(className)));
}
}
}
}
上诉就是我们手工实现的简单版IOC容器的注解方式。
下边是完整的Main类代码,方便直接拿着测试:
public class Main {
public static void main( String[] args) throws Exception {
//装 类 全路径的集合
HashSet<String> allClassPathSet = new HashSet<>();
doScanner(allClassPathSet, "com.czy.project.demo2");
for (String className : allClassPathSet) {
Class<?> clazz = Class.forName(className);//获取该class实例
//如果该类上有@CzyComponent注解,就直接将该类添加到ioc容器中
if (clazz.isAnnotationPresent(CzyComponent.class)) {
IOCContainer.putBean(className, clazz.newInstance());
}
//获取该该类中的所有方法
Method[] methods = clazz.getDeclaredMethods();
for (int i = 0; i < methods.length; i++) {
Method method = methods[i];
if (method.isAnnotationPresent(CzyBean.class)) { //如果该方法上有@CzyBean注解
//获取beanName例如@CzyBean("b")即获取出;b
String beanName = method.getAnnotation(CzyBean.class).value();
//判断该方法是否为静态方法
if (Modifier.isStatic(method.getModifiers())) {
IOCContainer.putBean(beanName, method.invoke(null));//静态工厂创建实例
} else {
//从容器中获取当前这实例对象
//实例工厂创建实例
IOCContainer.putBean(beanName, method.invoke(IOCContainer.getBean(className)));
}
}
}
}
}
/**
* 得到指定包下所有的class 的全路径
* @param allClassPathSet:装载类全路径的集合
* @param scanPackage:指定扫描的包路径
* 注意:该方法为Main类中的静态方法
*/
public static void doScanner(Set<String> allClassPathSet, String scanPackage) {
URL url = Main.class.getResource("/" + scanPackage.replaceAll("\\.", "/"));
File classDir = new File(url.getFile());
for (File file : classDir.listFiles()) {
if (file.isDirectory()) {
//是文件夹,递归循环
doScanner(allClassPathSet, scanPackage + "." + file.getName());
} else {
//如果文件不是.class结尾
if (!file.getName().endsWith(".class")) {
continue;
}
//拼装类的全路径
String clazzName = (scanPackage + "." + file.getName().replace(".class", ""));
//添加到集合中
allClassPathSet.add(clazzName);
}
}
}
}