一、反射
反射机制是java中一个很强大的功能,可以跳过jvm实例化对象的过程,直接实例化类的class对象,然后直接操作class对象,再运行过程中,几乎可以得到任何一个类的属性和方法,能够调用任何一个对象的方法和属性。
Class对象:一个类的描述,存储了类的信息。看一下api:
比如一个类叫Demo,在jvm加载的时候就会创建一个class对象,里边存储了Demo类的信息,有什么属性,方法,构造器,都在里边由描述了。但是对一个一个类来讲,就只有一个类对象,有多个实例也只用一个类对象。也就是说如果new两个Demo用的都是同一个class对象,class类是确实存在的一个类,是java.lang包下的一个类,所以它有很多方法,再调用之前需要先获得它的对象才行。
什么时候初始化Class类?
1.创建一个类的实例时,比如 new Demo()时
2.调用一个类的静态方法,访问一个类的静态变量的或者为静态变量赋值的时候。因为静态属性或方法是随着类的加载而加载,与对象无关,所以要想调用就需要先创建class对象。
3.使用反射强制创建某个类或者接口的class对象
4.使用java.exe命令直接运行某个主类时(比如直接在cmd中用java.exe来运行一个class文件)
当创建了class对象时,也就是说该类被加载了,只是不一定有实例对象,需要注意的是,静态代码是随着类的加载而加载,与对象无关。
class对象的获取方法
想获得一个类的class对象,有三种方式。下面直接用代码和注释说明。
public class Demo {
public static void main(String[] args) throws ClassNotFoundException {
//第一种,使用Object类的方法,getClass()
User u =new User();//创建User的Class对象
Class c1 = u.getClass();
//第二种,调用内置属性class
Class c2 = User.class;
//第三种,使用Class类的静态方法forName(全名限定)
Class c3 = Class.forName("wang.User");
System.out.println(c1==c2);
System.out.println(c2==c3);
}
}
这三种相对于方便一点的是第二种,第三种更万能一些,可以在项目的所有位置得到class对象,只要知道类的全名限定类名就可以。所以用的也比较多,但如果代码种已经有实例对象,直接获得对象的class对象最方便。但无论哪种方式,获得的都是同一个class对象,因为每个类只有一个class对象,两个打印都是true。
使用Class对象
得到class对象以后就可以获得存储的类信息。使用Class类的一些方法。
1.获得构造方法
批量获取构造方法:
public Constructor[] getConstructors():获取所有"公有"的构造方法,得到Constructor数组
public Constructor[] getDeclaredConstructors():获取所有的构造方法,包括私有、受保护、默认、公有
获取单个的构造方法:
public Constructor getConstructor(Class ... parameterTypes):获取某个"公有"的构造方法
public Constructor getDeclaredConstructor(Class ... parameterTypes):获取某个构造方法,包括私有、受保护、默认、公有
调用某个构造方法:
Constructor的newInstance()
Constructor类封装的是单个的构造方法信息并提供访问权限。
例子:给User类添加一个公共String参数构造方法,然后通过反射获取并调用
public class Demo {
public static void main(String[] args) throws Exception {
//第三种,使用Class类的静态方法forName(全名限定)
Class c3 = Class.forName("wang.User");
Constructor con = c3.getConstructor(String.class);
con.newInstance("小明");//调用方法并传值
}
}
这里要注意一个地方,就是con.newInstance("小明")这一步,我们说Class对象是类信息,但不是类实例,要想调用方法必须实例化一个User,newInstance就相当于new了一个User对象,在堆中开辟了一块空间,然后再调用的。对于任何一个类来讲,如果没有定义任何构造器,是默认有一个无参的构造器。
2.获得成员属性:和获取构造方法很像,结果用Field类封装。
批量获取成员属性:
public Field[] getFields():获取所有"公有"的成员属性
public Field[] getDeclaredFields():获取所有的成员属性,包括私有、受保护、默认、公有
获取单个的成员属性,并调用:
public Field getField(Class ... parameterTypes):获取某个"公有"的成员属性
public Field getDeclaredField(Class ... parameterTypes):获取某个成员属性,包括私有、受保护、默认、公有
为成员属性赋值:
Field类的set(Object target,Object value)
例子:获取User的name私有属性,并赋值(注意,赋值也要有实例对象,所以要先使用newInstance方法)
public class Demo {
public static void main(String[] args) throws Exception {
//第三种,使用Class类的静态方法forName(全名限定)
Class c3 = Class.forName("wang.User");
Field f= c3.getDeclaredField("name");//获取私有的属性
Object o = c3.getConstructor().newInstance();//调用构造器实例化对象
f.setAccessible(true);//如果是私有的属性访问前要设置“强制访问”
f.set(o,"小明");
}
}
3.获取成员方法:和前边两个很像,结果用Method类封装。
批量获取成员方法:
public Method[] getMethods():获取所有"公有"的成员方法
public Method[] getDeclaredMethods():获取所有的成员方法,包括私有、受保护、默认、公有
获取单个的成员方法,并调用:
public Method getMethod(Class ... parameterTypes):获取某个"公有"的成员方法
public Method getDeclaredMethod(Class ... parameterTypes):获取某个成员方法,包括私有、受保护、默认、公有
调用成员方法:
Method类的invoke(Object target,Object... args)
例子:获取User的show方法,调用并传入姓名和年龄
public static void main(String[] args) throws Exception {
//第三种,使用Class类的静态方法forName(全名限定)
Class c3 = Class.forName("wang.User");
Object o = c3.getConstructor().newInstance();//调用构造器实例化对象
Method m = c3.getMethod("show", String.class, int.class);
m.setAccessible(true);//如果是私有设置“暴力访问”
m.invoke(m,"小明",30);
}
以上就是获得class对象后如何获得类的信息,我们可以看到,无论是公有还是私有的,都可以访问到并调用,这就绕开了封装的保护,所以反射是一个很强大的功能,在java框架中有很多应用的地方。
利用反射运行配置文件
如果配置一个内置管理员用户,如果在User类中写死,name是“小明”,年龄20岁,那么如果我想变成“小李”,年龄30岁,或者想换一个新类,就需要写一个新的User2类,源代码中使用User的地方也需要更改,然后重新生成安装包,非常麻烦,但如果我们能将信息写道配置文件中,然后通过反射来完成,就可以不改变源代码也能完成转换。
例子:user1和user2,并调用类中的成员方法设置名字
,我们可以假定开发时需要废掉user1换成user2,那么源代码就需要改写,例如
public static void main(String[] args) throws Exception {
// User1 u =new User1();
// u.setName("小明");
User2 u =new User2(); u.setName("小李");
}
如果写成反射并使用配置文件,首先创建配置文件my.properties
#类的全名限定
ClassName=wang.User1
#方法名
MethodName=setName
#实参
name=小明
使用properties类读取
public static Object getValue(String key) throws IOException {
Properties p =new Properties();
//String path =Demo.class.getClassLoader().getResource("my.properties").getPath();
FileReader in =new FileReader("my.properties");//注意,需要本类的class文件可以访问到该资源才可以这样写,或者直接写成绝对路径,
// 因为idea默认的输出目录时out,所以class文件,配置文件和源码可能并不在一起,需要自己设置
//或者使用上边的写法,使用ClassLoader找到和class文件一起的资源
p.load(in);
in.close();
return p.getProperty(key);
}
改造源代码
public class Demo {
public static void main(String[] args) throws Exception {
Class c = Class.forName((String) getValue("ClassName"));
Method m = c.getMethod((String) getValue("MethodName"),String.class);
Object o = c.getConstructor().newInstance();
m.invoke(o,getValue("name"));
}
改成反射的形式,把设置名字的代码和具体的User1和User2解耦,直接读取配置文件中的内容,以后直接改配置文件即可,不需要修改源码,不需要重新安装和重启(不重启的前提是修改class文件能访问到的配置文件,而不是源文件能访问到的配置)等。
利用反射的java动态代理
所谓代理就是代为行事,在实际开发过程中会有类似这样的需求,需要在执行某个操作之前做一些事,之后做一些事,比如开启事务,提交事务,记录日志等等。这时候我们可以为原来的类写一个代理类。在代理类中完成操作。
比如写一个userService类来进行操作数据,有一个show方法。我想在show方法执行之前进行操作,或者之后执行操作,但是我不想改变show方法,因为有些地方用前后操作,这样就需要写一个代理类,代理类将show方法拿过来,可以复制,也可以继承。然后直接将操作加入进去即可。
public void show(){
beforeDo();
super.show();
endDo();
}
//之前要做的事
public void beforeDo(){
System.out.println("============方法之前做的");
}
//之后要做的事
public void endDo(){
System.out.println("============方法之后做的");
}
使用的时候直接使用代理类
public static void main(String[] args) {
UserServiceProxy u =new UserServiceProxy();
u.show();
}
那么得到同样的结果:
============方法之前做的
========我是show方法======
============方法之后做的
现实开发中,代理模式比这要复杂,为了代码的灵活性,一般会让目标类和代理类实现同一个接口,然在代理类中使用构造方法传入目标类,然后调用目标类的方法。但是道理和这个一样,就是用代理类执行目标类的方法,并加上特有的操作,完成代理。
但是这样会有一个问题,就是代理类完全取代了原来的类,也就是说如果我有很多这样的操作,比如加日志,就需要写很多的代理类,然后每个调用的地方都要改。这就很麻烦。所以java提供了一种基于反射机制的动态代理。使得加日志这样的业务可以单独拿出来配给所有需要的类,实现解耦合。主要用到两个类,1.proxy,2.InvocationHandler
proxy英文释义就是代理,这个好理解,就是这个类负责进行动态代理相关的业务。
InvocationHandler这是个接口,它就是我们实现自定义操作需要的接口。
主要步骤有三个。
1.写一个自定义类实现InvocationHandler接口,重写invoke方法,如果懒得写可以直接在newProxyInstance中直接使用内部类,直接new InvocationHandler,然后重写invoke方法。
public class MyHandler implements InvocationHandler {
private Object obj;
public MyHandler(Object obj) {
this.obj = obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
beforeDo();
Object invoke = method.invoke(obj, args);
endDo();
return invoke;
}
//之前要做的事
public void beforeDo(){
System.out.println("============方法之前做的");
}
//之后要做的事
public void endDo(){
System.out.println("============方法之后做的");
}
}
除了beforeDo和endDo前面基本都是固定写法。
2.将UserService写一个接口和一个实现类的形式,我把UserService写成接口,用UserServiceImpl实现。
public class UserServiceImpl implements UserService{
public void show(){
System.out.println("========我是show方法======");
}
3.使用proxy生成代理类。
public static void main(String[] args) {
UserService u = (UserService) Proxy.newProxyInstance(UserServiceImpl.class.getClassLoader(),//代理目标类的加载器
UserServiceImpl.class.getInterfaces(),//代理目标类所实现的接口
new MyHandler(new UserServiceImpl()));//自定义操作类
u.show();
}
结果;
============方法之前做的
========我是show方法======
============方法之后做的
proxy的newProxyInstance方法,就是创建代理类的方法。主要有三个参数,因为我的show方法在UserServicImpl中,那么我想在show方法前执行beforeDo,之后执行endDo,代理目标就是UserServicImpl类。我们要生成一个UserServiceImpl的代理类。Myhandler的参数传入代理目标。
newProxyInstance方法的返回值是一个Object,我们可以用UserServic来接收,然后调用show方法。这样一来,MyHandler就可以给所有想加BeforeDo和endDo的类使用,只需要使用proxy.newProxyInstance方法生成代理就可以。但是前提是要有第二步,你要代理的类需要有接口。因为proxy生成代理是基于接口的,第二个参数就是接口数组,所以必须有接口。这样就可以实现多态,接收的时候可以用代理目标类来接收,也可以用接口来接收。如果我再写一个CustomerServiceImpl也实现了UserService接口,也同样可以生成动态代理执行show方法。同样用userService接收,这就是多态。
在开发中,经常将mian方法设计所在的类设计成创建动态代理工厂类,main方法设计成创建方法,在需要实例化目标类时候通过代理工厂创建,直接得到代理对象。
那么动态代理实现是逻辑原理是什么呢,答案就是反射。如果我们看newProxyInstance方法的源码,就可以看到这样的代码
第一个是使用代理工厂根据loader(我们传入的加载器)和intfs(我们传入的接口)生成代理类,就想我们写的UserServiceProxy一样,然后用反射里的getConstructor方法获取构造方法,最后调用newInstance方法,使用h(我们传入的MyHandler)生成实例对象,返回给我们。所以我们得到的代理类实例对象是拥有了自定义操作的。
三、java的注解
注解是给程序的标注,用于描述程序如何运行及在什么阶段来运行,英文Java Annotation ,实际开发中,最大的功能是用于替换配置文件,注解有多种类型,平时我们常用的有jdk种的几种,剩下的可以自定义。
1.jdk中的三个常用基本注解
@Override:检查子类是否是覆盖了父类的方法。
@Deprecated:说明已经过时了。
@SuppressWarnings({ "unused", "deprecation" }):抑制程序中的警告。unused警告的类型。{}数组。all抑制所有警告。
2.自定义注解
自定义注解的语法:@interface 它和class interface,enum一样是关键字,形容一个注解类,它的实际作用是扩展了lang.Annotation包下的Annotation类,但是你不能用extends或者implements关键字显性的去扩展,只能用@interface 来修饰。
public @interface MyAnnotation {
}
在注解类种可以定义属性,类似与int age();这样的,带括号的,在注解中是属性不是方法。基本类型、String、枚举类型、注解类型、Class类型、以上类型的一维数组类型。
public @interface MyAnnotation {
int age() default 0;//基本数据类型
String name() default "";//字符串
String str() default "";//定义了default默认值,使用时该属性可以不传入
String[] value();//定义为value,使用是可以不显性的传入,其他的名称必须显性使用
//otherAnno anno();//其他的注解类
Class clazz();//class对象,不是普通对象,是类对象。
}
使用时
@MyAnnotation("abc")
public class Demo {
@MyAnnotation("abc")//多属性用逗号隔开,数组如果只传一个可以不加,
// 名字时value的属性如果只有value可以不写
public void show(){
System.out.println("执行了show方法");
}
}
到此,你使用@MyAnnotation只是对show方法进行了标注,现在没有任何意义。所以要讲到注解的灵魂,“注解的反射”
在反射的包下有一个接口AnnotatedElement,像Class类,Method,Filed,Constructor等类都实现了该接口,该接口有几个方法
- <T extends Annotation> T getAnnotation(Class<T> annotationType):获得指定类型的注解的引用。如果有返回指定类型的注解,没有返回null。
- Annotation[] getAnnotations():得到所有的注解,包含从父类继承下来的。
- Annotation[] getDeclaredAnnotations():得到自己身上的注解。
- boolean isAnnotationPresent(Class<? extends Annotation> annotationType):判断有没有使用指定注解。
比如我们在
public class MyTest {
public static void main(String[] args) {
//通过反射得到demo类所有的方法
Class<Demo> demoClass = Demo.class;
Method[] methods = demoClass.getMethods();
//使用isAnnotationPresent方法判断这些方法有没有使用MyAnnotation自定义注解,目前只有show方法使用
for (Method method : methods) {
boolean b = method.isAnnotationPresent(MyAnnotation.class);
System.out.println(method.getName()+"使用注解:"+b);
}
}
}
打印结构全部时false,为什么呢?明明show方法上边有我们的注解,这里引出另一个概念,“元注解”
注解上的注解叫做元注解。(即:用于修饰注解的注解),比如supperessWarnings的注解类上还有注解@Target和@retention,他们时修改注解的注解。
1.@Retention:作用。改变自定义的注解的存活范围。
那么自定义注解的存活范围时什么呢,默认时Class,也就是编译阶段,但是在运行阶段时不行的,所以要通过Retention重设,Retention接收一个RetentionPolicy,RetentionPolicy是一个枚举类,有三个值,SOURCE:源代码阶段,就是java文件,Class:编译阶段,class文件阶段,RUNTIME:运行阶段,加载阶段。
那么我们的MyAnnotation改 一下
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String[] value();
}
在运行MyTest,发现show放为true。说明 我们的设置生效了。如果现在我们想MyAnnotation只作用于show方法,不作用于demo类上,那么需要设置Target元注解
2.@Target:作用,指定该注解能用在什么地方。
@Target接收一个ElementType数组,ElementType是一个枚举类,里边是各种类型,比如TYPE表示类,FIELD属性,METHOD表示方法。
现在我们可以通过反射判断谁用了我们的自定义注解,如果判断为true,我们就可以模拟@Test注解,执行该方法(同过反射)
public static void main(String[] args) throws Exception {
//通过反射得到demo类所有的方法
Class<Demo> demoClass = Demo.class;
Method[] methods = demoClass.getMethods();
//使用isAnnotationPresent方法判断这些方法有没有使用MyAnnotation自定义注解,目前只有show方法使用
for (Method method : methods) {
boolean b = method.isAnnotationPresent(MyAnnotation.class);
if(b){
method.invoke(demoClass.newInstance(),null);
}
}
}
打印”执行了show方法“,在Demo类中所有使用了@MyAnnotation注解的方法都会被执行。这样自定义注解就有意了,我们还可以进一步使用注解中的属性,
if(b){
//到该方法上的注解对象
MyAnnotation at = method.getAnnotation(MyAnnotation.class);
String[] value = at.value();
if (value!=null){
System.out.println("使用了属性"+value[0]);
}
method.invoke(demoClass.newInstance(),null);
}