反射和注解的总结及举例
处于学习阶段,此处内容摘录自清茶博主,感谢!!!
静态编译与动态编译
静态编译:一次性编译,在编译的时候把所有的模块全部编译进去。其实项目中一般都是这种情况,因为动态编译在实际中很不可取,下面举个实例:
public class ReflectTest {
// 在启动时即将该方法用到的所有类全部编译
public static void main(String[] args) {
StaticClassTest staticClassTest = new StaticClassTest();
staticClassTest.say();
}
}
public class StaticClassTest {
public void say() {
System.out.println("hello");
}
}
如果把编译的StaticClassTest.class文件删除(在target目录下),再次运行即会报NoClassDefFoundError异常,只能重新编译再运行,下面看下动态编译的过程。
动态编译:按需编译,程序在运行的时候,需要哪个模块就编译哪个模块,下面先提前以反射的方式实现动态编译
public static void main(String[] args) {
System.out.println("请输入类的全路径,这里为了展现这种思想请填写指定的类型");
Scanner scanner = new Scanner(System.in);
String s = scanner.next();
try {
// 加载这个类
Class c = Class.forName(s);
// 获得这个类的实例化对象
Object obj = c.newInstance();
// 强制类型转换
StaticClassTest staticClassTest = (StaticClassTest) obj;
staticClassTest.say();
} catch (Exception e) {
}
}
输出结果:
然后当你把StaticClassTest.class文件删除之后(这里注意备份该class文件):
1、运行该方法并填入指定信息,此时必然还是报NoClassDefFoundError异常,与静态编译类型
2、再次运行该方法,然后恢复class文件,填入指定类信息,此时编译通过~
这样我们就可以在程序运行的情况下,动态的加载一个类,这里充分的体现了java的动态性。到这里静态编译与动态编译的区别应该很明显啦,不过在项目中还是不要用的好,因为:
a、存在安全问题,属于"注入漏洞",只要上传一个恶意的java程序就能让我们的安全工作毁于一旦
b、在高性能项目中也不要用,毕竟动态编译需要一个编译过程,且比静态编译多了一个执行环节
反射
有了上面静态编译与动态编译的入门,这里对反射的概念应该很好理解了,即在程序运行的过程中,能知道任何一个类及它的属性和方法,并且能够任意调用它的属性和方法。这种在程序运行时的动态获取机制就是java的反射机制,上面动态编译的例子其实就是运用了反射,下面讲下反射的常用api及使用方式。
*1、利用反射获取Class对象*
在反射中,要获取一个类的属性及方法,首先我们得先获取其Class对象,大致上有三种,d存在先决条件,b和c两种方式适合在编译前就知道要操作的Class
a、使用Class.forName()方法,通过传入类的全路径以获取该Class对象,上面动态编译的例子即是采用这种方式
Class clz = Class.forName("service.StaticClassTest");
b、使用类的.class方法获取
Class clz = StaticClassTest.class;
c、使用类对象的getClass()方法
StaticClassTest staticClassTest = new StaticClassTest();
Class clz = staticClassTest.getClass();
d、如果是基本类型的包装类,可以通过调用包装类的Type属性来获取其Class对象
Class clz = Integer.TYPE;
*2、通过反射创建类对象*
a、利用Class对象的newInstance()方法,上面动态编译的例子即是采用这种方式,调用的是该对象的默认无参构造方法
Class c = StaticClassTest.class;
StaticClassTest staticClassTest = (StaticClassTest) c.newInstance();
b、利用Constructor对象的newInstance()方法,好处是可以指定特定的构造方法创建类对象,看下面demo
Class clz = StaticClassTest.class;
Constructor constructor = clz.getConstructor(String.class, Integer.class);
StaticClassTest staticClassTest = (StaticClassTest) constructor.newInstance("小明", 25);
*3、利用反射获取类的属性、方法及构造器*
a、通过调用Class对象的getFields()获取class类的公有属性,但无法获取私有属性
b、通过调用getDeclaredFields()获取class类的所有属性,包括私有属性在内
输出如左图:
获取类的方法及构造器与获取属性类似,如果想要获取私有方法或私有构造器,则必须使用含有 declared 关键字的方法。
注解
之所以把注解和反射一起讲是因为在实际开发过程中,反射其实用的并不多,一般都是和注解联合使用,这里先把注解相关知识拎出来复习巩固下。
*1、注解的概念*
注解又叫元数据,提供了一种安全的类似注释的机制,是自jdk1.5以后所引入的一个作用在代码层面的说明,与类、接口、枚举是在同一个层次,以@interface关键词表示。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释,可以起到减少配置的成果,给程序起到辅助性的作用
*2、注解的作用,其实很好理解,对应着注解的三种作用域分类,下面会讲~*
插一句题外话,@SuppressWarnings最常抑制的两种警告类型分别是deprecation和unchecked。
b、编译时动态处理,动态生成代码,如
c、运行时动态处理,获得注解信息,如
*3、注解的分类,*有两种分法,第一种就是根据注解的类型分,一般可分为如下3中,另外一种分法是根据注解的作用域分,可分为源码时注解、编译时注解、运行时注解,对应于上面注解的三种作用,也对应于下面@Retention元注解所示的在哪种级别保存该注解信息
a、基本内置注解,是指Java自带的几个Annotation,如@Override、@Deprecated、@SuppressWarnings等
b、元注解(meta-annotation),是指负责注解其他注解的注解,JDK 1.5及以后版本定义了4个标准的元注解类型,如下:
1、@Target
2、@Retention
3、@Documented
4、@Inherited
c、自定义注解,根据需要可以自定义注解,自定义注解需要用到上面的元注解
*4、元注解相关信息*
a、@Target:指注解所修饰的对象范围,通过ElementType取值有8种,如下:
TYPE:类、接口(包括注解类型)或枚举
FIELD:属性
METHOD:方法
PARAMETER:参数
CONSTRUCTOR:构造函数
LOCAL_VARIABLE:局部变量
ANNOTATION_TYPE:注解类型
PACKAGE:包
b、@Retention:指注解被保留的时间长短,通过RetentionPolicy取值有3中,如下:
SOURCE:在源文件中有效(即源文件保留)
CLASS:在class文件中有效(即class保留)
RUNTIME:在运行时有效(即运行时保留)
c、@Documented:是一个标记注解,表示被标注的注解应该javadoc工具记录,默认情况下,javadoc是不包括注解的. 但如果声明注解时指定了@Documented,则它会被 javadoc 之类的工具处理,,所以注解类型信息也会被包括在生成的文档中
d、@Inherited:也是一个标记注解,表示某个被标注的注解是可以被继承的
自定义注解的应用
注解几乎无处不在,有注解的地方就有反射调用,自定义注解最常用的地方应该为通过自定义注解实现aop把,这个例子以前我好像写过一篇,传送门:
由反射+注解联想IOC+DI
其实看完反射+注解的记录后,到这里应该对Spring的控制反转和依赖注入有了更好的理解
IOC:将对象的创建权交给容器,我们无需在程序中主动去创建对象,spring的xml配置文件中的<bean>标签及一系列使用了全路径的类名,全部都是通过反射的方式(Class.forName())创建对象,这也算是反射在框架中的一个应用,不过用的更多的地方应该在动态代理中。
<bean id="staticClassTest" class="service.StaticClassTest"/>
DI:简单来说就是由容器动态的将某个依赖关系注入到组件之中,spring的依赖注入大致分为xml配置注入(属性注入、构造函数注入、工厂方法注入)和注解方式注入(如@Autowired等),注解注入在我们日常工作中可谓是平常的不能再平常了,即使我们不使用第三方注入,日常代码中也有依赖注入的影子,如下:
public class Human {
...
Father father;
...
public Human(Father father) {
this.father = father;
}
}
上面代码中,我们将 father 对象作为构造函数的一个参数传入,在调用 Human 的构造方法之前外部就已经初始化好了 Father 对象。像这种非自己主动初始化依赖,而通过外部来传入依赖的方式,我们就称为依赖注入。
反射+注解实际使用
其实反射在实际开发中用的并不算多,甚至能避免就避免,因为好多人都吐槽反射的效率不高代码可读性差等等balabala,但其实如果在某些动态的业务需求下使用反射+注解也是可行的,其带来的所谓性能问题可以忽略不计,
a、在方法上使用注解,适合在同一个类下多种实现
下面我们模拟第一个需求场景,练习一下反射+注解知识,以分页查询为例,根据前端传来的某个查询请求参数(例如只想要分页列表参数page,某种类型数据总数count),根据注解+反射在service层动态调用logic层接口,这样可以做到针对不同的查询方式,service层只需要一个接口就够了,逻辑可以都放在logic层,先讲下大致思路:
在进程启动时作一个初始化操作,将那些被注解所标识的接口方法全部放到一个指定map中,然后在所需要的地方直接通过反射调用,下面看下代码实现:
1) 先看自定义的注解
2) logic层实现类中的方法,即所需要扫描的类
// 该类中可以存放针对多种场景下的不同接口形式
3) 注解管理类中特定属性及方法,以及模拟service层调用逻辑
/**
* 存放指定类下的方法集
*/
private static Map<QueryType, Method> methodMap = new HashMap<>();
/**
* 需要扫面的类数组
*/
private static Class[] classes = new Class[] {ScanTest01.class};
// 上文所说的在进程启动时调用可用@PostConstruct注解实现
// 扫描指定class下所有被指定注解所标识的方法
for (Class clz : classes) {
Method[] methods = clz.getMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(ScoreAno.class)) {
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
if (!(annotation instanceof ScoreAno))
continue;
ScoreAno scoreAno = (ScoreAno) annotation;
QueryType queryType = new QueryType(scoreAno.filterType(), scoreAno.queryType());
methodMap.put(queryType, method);
}
}
}
}
// 这里演示在service层通过反射调用
Method method = getMethod("test", ScoreAno.QueryType.PAGE);
try {
PageResult<User> result = (PageResult<User>) method.invoke("123", "小明");
} catch (IllegalAccessException | InvocationTargetException e) {
}
// 封装获取指定方法
public Method getMethod(String fielterType, ScoreAno.QueryType queryType) {
return methodMap.get(new QueryType(fielterType, queryType));
}
/**
* 内部类
*/
private class QueryType {
private String fielterType;
private ScoreAno.QueryType queryType;
public QueryType(String fielterType, ScoreAno.QueryType queryType) {
this.fielterType = fielterType;
this.queryType = queryType;
}