注解是Java SE5中引入的总要特性,注解的出现使得我们能够以将有编译器来测试和验证的格式,存储有关程序的额外信息.
Java 中的元注解主要有以下四个
上面提到@Retention注解的取值有Source,Class,Runtime这三个,这三个值的意思如下
有了Java中提供的元注解,我们就可以依赖元注解来编写自定义注解了,我们只需要指定两个关键信息:注解在哪个级别可用,以及注解作用的位置就可以了,即给我们的注解打上@Target和@Retention注解。我们来看一个例子,下面的代码声明了一个定义注解MyAnnotion
package com.nightcat.annotaion; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { }
可以看到@Target制定了我们自定义的注解可以使用在类上,@Rentention中的保存策略指定了我们的注解将在运行期保留(这很重要)。
首先我们为自定义注解添加一些属性:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { String title() default ""; int type() default 0; }
我们向自定义注解中添加了一个String类型的title属性和一个int类型的type属性。
那么自定义注解中可以使用的元素类型有哪些呢?
我们定义好了注解,它能做些什么?这就必须提到注解处理器,因为如果没有用来处理注解的工具,那注解也不会有什么用处,那么什么是注解处理器呢?其实就是由我们自己编写的处理自定义注解的程序。下面我们就来编写一个注解处理器,用来处理我们的自定义注解。
由于我们的注解保存策略指定为Runtime,那么我们就能通过反射技术来获取注解。我们先来看一段代码
package com.nightcat.bean; import com.nightcat.annotaion.MyAnnotation; @MyAnnotation(title = "《好好学习》", type = 1) public class Article { }
我们声明了一个Article类,并且给它打上了我们自定义的注解,下面我们通过自定义的注解处理器来获取注解中属性的值。
package com.nightcat.test; import com.nightcat.annotaion.MyAnnotation; import com.nightcat.bean.Article; public class AnnotationTest { public static void main(String[] args) { Class<Article> articleClass = Article.class; MyAnnotation annotation = articleClass.getAnnotation(MyAnnotation.class); System.out.println("annotation.title() = " + annotation.title()); //《好好学习》 System.out.println("annotation.type() = " + annotation.type());// 1 } }
你会发现,我们用了反射技术获取到了定义注解的属性值,这就是自定义注解的使用方法:
学习了上面的知识,我们对注解有了一个基本认识,那么注解究竟是什么呢?
其实注解就是继承了Annotion接口的一个接口而已,拿我们自定义的MyAnnotation举例子:
我们先来看一下MyAnnotation.class文件
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package com.nightcat.annotaion; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { String title() default ""; int type() default 0; }
这是IDEA替我们处理好的,接着我们用javap -c命令反编译一下看看
通过反编译我们发现注解其实就是一个接口。
有了上面的基础我们终于要开始写自己的@Component注解了。我们知道SpringFramework中常用的注解有 @Component @Controller @Service @Bean @Repository 等等,这些注解有一个基础的功能,将注解标记的类加到Spring容器中,交给Spring管理。那么这就涉及到两个问题:
好了,接下来我们就开始写代码了
package com.nightcat.annotaion; 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.TYPE) public @interface MyComponent { }
你会发现我们的这个注解的保存策略是Runtime,原因不再赘述。
package com.nightcat.bean; import com.nightcat.annotaion.MyComponent; @MyComponent public class Student { private Integer age; private String stuName; public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getStuName() { return stuName; } public void setStuName(String stuName) { this.stuName = stuName; } @Override public String toString() { return "Student{" + "age=" + age + ", stuName='" + stuName + '\'' + '}'; } }
注意这个类上打上了我们自定义的@MyComponent注解,方便检测哪些类需要加入到ioc容器,哪些不需要。
我们在使用ioc容器时,主要使用的是ClassPathXmlApplicationContext或者AnnotationConfigApplicationContext,通过这两个类的构造得到ioc容器,然后通过getBean方法获取指定的bean,这个过程涉及到BeanFactory,需要判断以下内容
等等。
我们这里不这么麻烦,直接通过一个线程安全的HashMap来模拟oc容器
主要实现思路如下
package com.nightcat; import com.nightcat.annotaion.MyComponent; import java.io.File; import java.net.URL; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Logger; public class ApplicationContext { private Logger logger = Logger.getLogger(ApplicationContext.class.toString()); private Map<String, Object> ioc = new ConcurrentHashMap(); public Object getBean(String beanName) { return ioc.get(beanName); } public ApplicationContext(String packagePath) { scanPackagePath(packagePath); } private void scanPackagePath(String packagePath) { File file = getFile(packagePath); File[] allFile = getAllFile(packagePath, file); handleClassFile(packagePath, allFile); } private void handleClassFile(String packagePath, File[] allClassFile) { for (File file : allClassFile) { String classFileName = file.getName(); String fileName = classFileName.substring(0, classFileName.lastIndexOf(".")); String beanName = String.valueOf(fileName.charAt(0)).toLowerCase() + fileName.substring(1); String fullClassName = packagePath + "." + fileName; try { Class<?> aClass = Class.forName(fullClassName); if (aClass.isAnnotationPresent(MyComponent.class)) { Object o = aClass.newInstance(); ioc.put(beanName, o); } } catch (Exception e) { logger.warning("实例化异常"); } } } private File[] getAllFile(String packagePath, File file) { File[] files = file.listFiles(file1 -> { if (file1.isDirectory()) { scanPackagePath(packagePath + "." + file1.getName()); } else { String name = file1.getName(); if (name.endsWith(".class")) return true; else return false; } return false; }); return files; } private File getFile(String packagePath) { String dirPath = packagePath.replaceAll("\\.", "/"); URL url = this.getClass().getClassLoader().getResource(dirPath); String fileName = url.getFile(); return new File(fileName); } }
通过上面的代码,我们会发现,我们传入的basePackage也就是包名,是以点号分割的,而我们磁盘上的文件是以斜线分割的,所以我们要将点号替换成斜线
再接着我们获取packagePath对应的url资源,通过它获取文件的全路径名,有些小伙伴可能会说,哪里用这么麻烦,我直接用packagePath new File(String path)的形式创建文件不行么,干嘛用url啊,这是因为你传入的类似com.nightcat这种包名,你去直接newFile ,是不是发现少了一些东西?没错,就是你的磁盘路径,发现了么,而Java提供的Url类可以解决这个问题。
剩下的工作就简单了,我们去扫描传入的包路径和自路径,并且检测类上是否有我们自定义的注解,如果有,我们就用创建对象,同时将类名首字母小写,作为ConcurrentHashMap的key,创建的对象作为Value就可以了。
有些小伙伴可能会问,你这个Bean无法保证单例呀,也很简单,你写代码的时候判断一下容器里有没有,有了就不添加了。至于其他的知识,不在这篇文章的范畴,可以自行研究。
package com.nightcat.test; import com.nightcat.ApplicationContext; import com.nightcat.bean.Student; public class CodeTest { public static void main(String[] args) { ApplicationContext context = new ApplicationContext("com.nightcat"); Student student = (Student) context.getBean("student"); System.out.println(student); } }
通过测试,发现我们自定义的@MyComponet注解生效了。