APP下载

5年经验的Java工程师面试答不出反射和动态代理 怕是只会CRUD哦

消息来源:baojiabao.com 作者: 发布时间:2024-05-17

报价宝综合消息5年经验的Java工程师面试答不出反射和动态代理 怕是只会CRUD哦

分享阿里 P8 高阶架构师吐血总结的 《BATJ大厂高阶Java必问面试学习视讯》,附送 100G 面试学习视讯文件

阿里 P8 级高阶架构师吐血总结的面试学习视讯, 内容覆盖很广,分散式快取、RPC 呼叫、Zookeeper、讯息伫列、分散式搜索引擎、分散式 session、分库分表等。

另外,附送 100G 学习、面试视讯文件哟~

获取方式:加Qun:101-7599-436,即可免费无套路获取哦~

以下是资源的部分目录以及内容截图:

重要的事再说一遍,获取方式:加Qun:101-7599-436,即可免费无套路获取哦~

正文开始

一、反射概述

反射机制指的是Java在执行时候有一种自观的能力,能够了解自身的情况为下一步做准备,其想表达的意思就是:在执行状态中,对于任意一个类,都能够获取到这个类的所有属性和方法;对于任意一个物件,都能够呼叫它的任意一个方法和属性(包括私有的方法和属性),这种动态获取的资讯以及动态呼叫物件的方法的功能就称为java语言的反射机制。通俗点讲,通过反射,该类对我们来说是完全透明的,想要获取任何东西都可以,这是一种动态获取类的资讯以及动态呼叫物件方法的能力。

想要使用反射机制,就必须要先获取到该类的字节码档案物件(.class),通过该类的字节码物件,就能够通过该类中的方法获取到我们想要的所有资讯(方法,属性,类名,父类名,实现的所有界面等等),每一个类对应着一个字节码档案也就对应着一个Class型别的物件,也就是字节码档案物件

Java提供的反射机制,依赖于我们下面要讲到的Class类和java.lang.reflect类库。我们下面要学习使用的主要类有:①Class表示类或者界面;②java.lang.reflect.Field表示类中的成员变数;③java.lang.reflect.Method表示类中的方法;④java.lang.reflect.Constructor表示类的构造方法;⑤Array提供动态阵列的建立和访问阵列的静态方法。

回到顶部

二、反射之Class类

(1)初识Class类

在类Object下面提供了一个方法:

,此方法将会被所有的子类继承,该方法的返回值为一个Class,这个Class类就是反射的源头。那么Class类是什么呢?Class类是Java中用来表达执行时型别资讯的对应类,我们刚刚也说过所有类都会继承Object类的getClass方法,那么也体现了著Java中的每个类都有一个Class物件,当我们编写并编译一个建立的类就会产生对应的class档案并将类的资讯写到该class档案中,当我们使用正常方式new一个物件或者使用类的静态字段时候,JVM的累加器子系统就会将对应的Class物件载入到JVM中,然后JVM根据这个型别资讯相关的Class物件建立我们需要的例项物件或者根据提供静态变数的引用值。将Class类称为类的型别,一个Class物件称为类的型别物件。

(2)Class有下面的几个特点

①Class也是类的一种(不同于class,class是一个关键字);

②Class类只有一个私有的建构函式

,只有JVM能够建立Class类的例项;

③对于同一类的物件,在JVM中只存在唯一一个对应的Class类例项来描述其资讯;

④每个类的例项都会记得自己是由哪个Class例项所生成;

⑤通过Class可以完整的得到一个类中的完整结构;

(3)获取Class类例项

刚刚说到过Class只有一个私有的建构函式,所以我们不能通过new建立Class例项 ,有下面这几种获取Class例项的方法:

①Class.forName("类的全限定名"),该方法只能获取引用型别的类型别物件。该方法会丢掷异常(a.l类载入器在类路径中没有找到该类 b.该类被某个类载入器载入到JVM内存中,另外一个类载入器有尝试从同一个包中载入)

1 //Class clazz = Class.forName("类的全限定名");这是通过Class类中的静态方法forName直接获取一个Class的物件

2 Class> clazz1 = null;

3 try {

4 clazz1 = Class.forName("reflect.Person");

5 } catch (ClassNotFoundException e) {

6 e.printStackTrace;

7 }

8 System.out.println(clazz1); //class reflect.Person

②如果我们有一个类的物件例项,那么通过这个物件的getClass方法可以获得他的Class物件,如下所示

1 //Class clazz = xxx.getClass; //通过类的例项获取类的Class物件

2 Class> clazz3 = new Person.getClass;

3 System.out.println(clazz3); //class reflect.Person

5 Class> stringClass = "string".getClass;

6 System.out.println(stringClass); //class java.lang.String

8 /**

9 * [代表阵列,

10 * B代表byte;

11 * I代表int;

12 * Z代表boolean;

13 * L代表引用型别

14 * 组合起来就是指定型别的一维阵列,如果是[[就是二维阵列

15 */

16 Class> arrClass = new byte[20].getClass;

17 System.out.println(arrClass); //class [B

18

19 Class> arrClass1 = new int[20].getClass;

20 System.out.println(arrClass1); //class [I

21

22 Class> arrClass2 = new boolean[20].getClass;

23 System.out.println(arrClass2); //class [Z

24

25 Class> arrClass3 = new Person[20].getClass;

26 System.out.println(arrClass3); //class [Lreflect.Person;

27

28 Class> arrClass4 = new String[20].getClass;

29 System.out.println(arrClass4); //class [Ljava.lang.String;

③通过类的class字节码档案获取,通过类名.class获取该类的Class物件

1 //Class clazz = XXXClass.class; 当类已经被载入为.class档案时候,

2 Class clazz2 = Person.class;

3 System.out.println(clazz2);

4 System.out.println(int .class);//class [[I

5 System.out.println(Integer.class);//class java.lang.Integer

(4)关于包装类的静态属性

我们知道,在Java中对于基本型别和void都有对应的包装类。在包装类中有一个静态属性TYPE储存了该类的类型别。如下所示

1 /**

2 * The {@code Class} instance representing the primitive type

3 * {@code int}.

4 *

5 * @since JDK1.1

6 */

7 @SuppressWarnings("unchecked")

8 public static final Class TYPE = (Class) Class.getPrimitiveClass("int");

我们使用这个静态属性来获得Class例项,如下所示

1 Class c0 = Byte.TYPE; //byte

2 Class c1 = Integer.TYPE; //int

3 Class c2 = Character.TYPE; //char

4 Class c3 = Boolean.TYPE; //boolean

5 Class c4 = Short.TYPE; //short

6 Class c5 = Long.TYPE; //long

7 Class c6 = Float.TYPE; //float

8 Class c7 = Double.TYPE; //double

9 Class c8 = Void.TYPE; //void

(5)通过Class类的其他方法获取

①public native Class super T> getSuperclass:获取该类的父类

1 Class c1 = Integer.class;

2 Class par = c1.getSuperclass;

3 System.out.println(par); //class java.lang.Number

②public Class> getClasses:获取该类的所有公共类、界面、列举组成的Class阵列,包括继承的;

③public Class> getDeclaredClasses:获取该类的显式宣告的所有类、界面、列举组成的Class阵列;

④(Class/Field/Method/Constructor).getDeclaringClass:获取该类/属性/方法/构造器所在的类

回到顶部

三、Class类的API

这是下面测试用例中使用的Person类和实现的界面

Person

1、建立例项物件

1 public void test4 throws Exception{

2 Class clazz =Class.forName("reflect.Person");

3 Person person = (Person)clazz.newInstance;

4 System.out.println(person);

5 }

建立执行时类的物件,使用newInstance,实际上就是呼叫执行时指定类的无参构造方法。这里也说明要想建立成功,需要对应的类有无参构造器,并且构造器的许可权要足够,否则会丢掷下面的异常。

①我们显示宣告Person类一个带参构造,并没有无参构造,这种情况会丢掷InstantiationException

②更改无参构造器访问许可权为private

2、获取构造器

(1)获取指定可访问的构造器建立物件例项

上面我们所说的使用newInstance方法建立物件,如果不指定任何引数的话预设是呼叫指定类的无参构造器的。那么如果没有无参构造器,又想建立物件例项怎么办呢,就使用 Class类提供的获取构造器的方法,显示指定我们需要呼叫哪一个无参构造器。

1 @Test

2 public void test5 throws Exception {

3 Class clazz = Class.forName("reflect.Person");

4 //获取带参构造器

5 Constructor constructor = clazz.getConstructor(String.class, String .class);

6 //通过构造器来例项化物件

7 Person person = (Person) constructor.newInstance("p1", "person");

8 System.out.println(person);

9 }

当我们指定的构造器全部不够(比如设定为private),我们在呼叫的时候就会丢掷下面的异常

(2)获得全部构造器

1 @Test

2 public void test6 throws Exception {

3 Class clazz1 = Class.forName("reflect.Person");

4 Constructor constructors = clazz1.getConstructors;

5 for (Constructor constructor : constructors) {

6 Class parameters = constructor.getParameterTypes;

7 System.out.println("建构函式名:" + constructor + " " + "引数");

8 for (Class c: parameters) {

9 System.out.print(c.getName + " ");

10 }

11 System.out.println;

12 }

13 }

执行结果如下

3、获取成员变数并使用Field物件的方法

(1)Class.getField(String)方法可以获取类中的指定字段(可见的), 如果是私有的可以用getDeclaedField("name")方法获取,通过set(物件引用,属性值)方法可以设定指定物件上该字段的值, 如果是私有的需要先呼叫setAccessible(true)设定访问许可权,用获取的指定的字段呼叫get(物件引用)可以获取指定物件中该字段的值。

1 @Test

2 public void test7 throws Exception {

3 Class clazz1 = Class.forName("reflect.Person");

4 //获得例项物件

5 Person person = (Person) clazz1.newInstance;

6 /**

7 * 获得类的属性资讯

8 * 使用getField(name),通过指定的属性name获得

9 * 如果属性不是public的,使用getDeclaredField(name)获得

10 */

11 Field field = clazz1.getDeclaredField("id");

12 //如果是private的,需要设定许可权为可访问

13 field.setAccessible(true);

14 //设定成员变数的属性值

15 field.set(person, "person1");

16 //获取成员变数的属性值,使用get方法,其中第一个引数表示获得字段的所属物件,第二个引数表示设定的值

17 System.out.println(field.get(person)); //这里的field就是id属性,打印person物件的id属性的值

18 }

(2)获得全部成员变数

1 @Test

2 public void test8 throws Exception{

3 Class clazz1 = Class.forName("reflect.Person");

4 //获得例项物件

5 Person person = (Person) clazz1.newInstance;

6 person.setId("person1");

7 person.setName("person1_name");

8 Field fields = clazz1.getDeclaredFields;

9 for (Field f : fields) {

10 //开启private成员变数的可访问许可权

11 f.setAccessible(true);

12 System.out.println(f+ ":" + f.get(person));

13 }

14 }

4、获取方法并使用method

(1)使用Class.getMethod(String, Class...) 和 Class.getDeclaredMethod(String, Class...)方法可以获取类中的指定方法,如果为私有方法,则需要开启一个许可权。setAccessible(true);用invoke(Object, Object...)可以呼叫该方法。如果是私有方法而使用的是getMethod方法来获得会丢掷NoSuchMethodException

1 @Test

2 public void test9 throws Exception{

3 Class clazz1 = Class.forName("reflect.Person");

4 //获得例项物件

5 Person person = (Person) clazz1.newInstance;

6 person.setName("Person");

7 //①不带引数的public方法

8 Method playBalls = clazz1.getMethod("playBalls");

9 //呼叫获得的方法,需要指定是哪一个物件的

10 playBalls.invoke(person);

11

12 //②带参的public方法:第一个引数是方法名,后面的可变引数列表是引数型别的Class型别

13 Method sing = clazz1.getMethod("sing",String.class);

14 //呼叫获得的方法,呼叫时候传递引数

15 sing.invoke(person,"HaHaHa...");

16

17 //③带参的private方法:使用getDeclaredMethod方法

18 Method dance = clazz1.getDeclaredMethod("dance", String.class);

19 //呼叫获得的方法,需要先设定许可权为可访问

20 dance.setAccessible(true);

21 dance.invoke(person,"HaHaHa...");

22 }

(2)获得所有方法(不包括构造方法)

1 @Test

2 public void test10 throws Exception{

3 Class clazz1 = Class.forName("reflect.Person");

4 //获得例项物件

5 Person person = (Person) clazz1.newInstance;

6 person.setName("Person");

7 Method methods = clazz1.getDeclaredMethods;

8 for (Method method: methods) {

9 System.out.print("方法名" + method.getName + "的引数是:");

10 //获得方法引数

11 Class params = method.getParameterTypes;

12 for (Class c : params) {

13 System.out.print(c.getName + " ");

14 }

15 System.out.println;

16 }

17 }

5、获得该类的所有界面

Class getInterfaces:确定此物件所表示的类或界面实现的界面,返回值:界面的字节码档案物件的阵列

1 @Test

2 public void test11 throws Exception{

3 Class clazz1 = Class.forName("reflect.Person");

4 Class interfaces = clazz1.getInterfaces;

5 for (Class inter : interfaces) {

6 System.out.println(inter);

7 }

8 }

6、获取指定资源的输入流

InputStream getResourceAsStream(String name),返回值:一个 InputStream 物件;如果找不到带有该名称的资源,则返回 null;引数:所需资源的名称,如果以"/"开始,则绝对资源名为"/"后面的一部分。

1 @Test

2 public void test12 throws Exception {

3 ClassLoader loader = this.getClass.getClassLoader;

4 System.out.println(loader);//[email protected] ,应用程序类载入器

5 System.out.println(loader.getParent);//[email protected] ,扩充套件类载入器

6 System.out.println(loader.getParent.getParent);//null ,不能获得启动类载入器

8 Class clazz = Person.class;//自定义的类

9 ClassLoader loader2 = clazz.getClassLoader;

10 System.out.println(loader2);//[email protected]

11

12 //下面是获得InputStream的例子

13 ClassLoader inputStreamLoader = this.getClass.getClassLoader;

14 InputStream inputStream = inputStreamLoader.getResourceAsStream("person.properties");

15 Properties properties = new Properties;

16 properties.load(inputStream);

17 System.out.println("id:" + properties.get("id"));

18 System.out.println("name:" + properties.get("name"));

19 }

其中properties档案内容

五、反射的应用之动态代理

代理模式在Java中应用十分广泛,它说的是使用一个代理将物件包装起来然后用该代理物件取代原始物件,任何原始物件的呼叫都需要通过代理物件,代理物件决定是否以及何时将方法呼叫转到原始物件上。这种模式可以这样简单理解:你自己想要做某件事情(被代理类),但是觉得自己做非常麻烦或者不方便,所以就叫一个另一个人(代理类)来帮你做这个事情,而你自己只需要告诉要做啥事就好了。上面我们讲到了反射,在下面我们会说一说java中的代理

1、静态代理

静态代理其实就是程式执行之前,提前写好被代理类的代理类,编译之后直接执行即可起到代理的效果,下面会用简单的例子来说明。在例子中,首先我们有一个顶级界面(ProductFactory),这个界面需要代理类(ProxyTeaProduct)和被代理类(TeaProduct)都去实现它,在被代理类中我们重写需要实现的方法(action),该方法会交由代理类去选择是否执行和在何处执行;被代理类中主要是提供顶级界面的的一个引用但是引用实际指向的物件则是实现了该界面的代理类(使用多型的特点,在代理类中提供构造器传递实际的物件引用)。分析之后,我们通过下面这个图理解一下这个过程。

1 package proxy;

3 /**

4 * 静态代理

5 */

6 //产品界面

7 interface ProductFactory {

8 void action;

9 }

10

11 //一个具体产品的实现类,作为一个被代理类

12 class TeaProduct implements ProductFactory{

13 @Override

14 public void action {

15 System.out.println("我是生产茶叶的......");

16 }

17 }

18

19 //TeaProduct的代理类

20 class ProxyTeaProduct implements ProductFactory {

21 //我们需要ProductFactory的一个实现类,去代理这个实现类中的方法(多型)

22 ProductFactory productFactory;

23

24 //通过构造器传入实际被代理类的物件,这时候代理类呼叫action的时候就可以在其中执行被代理代理类的方法了

25 public ProxyTeaProduct(ProductFactory productFactory) {

26 this.productFactory = productFactory;

27 }

28

29 @Override

30 public void action {

31 System.out.println("我是代理类,我开始代理执行方法了......");

32 productFactory.action;

33 }

34 }

35 public class TestProduct {

36

37 public static void main(String args) {

38 //建立代理类的物件

39 ProxyTeaProduct proxyTeaProduct = new ProxyTeaProduct(new TeaProduct);

40 //执行代理类代理的方法

41 proxyTeaProduct.action;

42 }

43 }

那么程式测试的输出结果也很显然了,代理类执行自己实现的方法,而在其中有呼叫了被代理类的方法

那么我们想一下,上面这种称为静态代理的方式有什么缺点呢?因为每一个代理类只能为一个借口服务(因为这个代理类需要实现这个界面,然后去代理界面实现类的方法),这样一来程式中就会产生过多的代理类。比如说我们现在又来一个界面,那么是不是也需要提供去被代理类去实现它然后交给代理类去代理执行呢,那这样程式就不灵活了。那么如果有一种方式,就可以处理新新增界面的以及实现那不就更加灵活了吗,在java中反射机制的存在为动态代理创造了机会

2、JDK中的动态代理

动态代理是指通过代理类来呼叫它物件的方法,并且是在程式执行使其根据需要建立目标型别的代理物件。它只提供一个代理类,我们只需要在执行时候动态传递给需要他代理的物件就可以完成对不同界面的服务了。看下面的例子。(JDK提供的代理正能针对界面做代理,也就是下面的newProxyInstance返回的必须要是一个界面)

1 package proxy;

3 import java.lang.reflect.InvocationHandler;

4 import java.lang.reflect.Method;

5 import java.lang.reflect.Proxy;

7 /**

8 * JDK中的动态代理

9 */

10 //第一个界面

11 interface TargetOne {

12 void action;

13 }

14 //第一个界面的被代理类

15 class TargetOneImpl implements TargetOne{

16 @Override

17 public void action {

18 System.out.println("我会实现父界面的方法...action");

19 }

20 }

21

22

23 //动态代理类

24 class DynamicProxyHandler implements InvocationHandler {

25 //界面的一个引用,多型的特性会使得在程式执行的时候,它实际指向的是实现它的子类物件

26 private TargetOne targetOne;

27 //我们使用Proxy类的静态方法newProxyInstance方法,将代理物件伪装成那个被代理的物件

28 /**

29 * ①这个方法会将targetOne指向实际实现界面的子类物件

30 * ②根据被代理类的资讯返回一个代理类物件

31 */

32 public Object setObj(TargetOne targetOne) {

33 this.targetOne = targetOne;

34 // public static Object newProxyInstance(ClassLoader loader, //被代理类的类载入器

35 // Class> interfaces, //被代理类实现的界面

36 // InvocationHandler h) //实现InvocationHandler的代理类物件

37 return Proxy.newProxyInstance(targetOne.getClass.getClassLoader,targetOne.getClass.getInterfaces,this);

38 }

39 //当通过代理类的物件发起对界面被重写的方法的呼叫的时候,都会转换为对invoke方法的呼叫

40 @Override

41 public Object invoke(Object proxy, Method method, Object args) throws Throwable {

42 System.out.println("这是我代理之前要准备的事情......");

43 /**

44 * 这里回想一下在静态代理的时候,我们显示指定代理类需要执行的是被代理类的哪些方法;

45 * 而在这里的动态代理实现中,我们并不知道代理类会实现什么方法,他是根据执行时通过反射来

46 * 知道自己要去指定被代理类的什么方法的

47 */

48 Object returnVal = method.invoke(this.targetOne,args);//这里的返回值,就相当于真正呼叫的被代理类方法的返回值

49 System.out.println("这是我代理之后要处理的事情......");

50 return returnVal;

51 }

52 }

53 public class TestProxy {

54 public static void main(String args) {

55 //建立被代理类的物件

56 TargetOneImpl targetOneImpl = new TargetOneImpl;

57 //建立实现了InvocationHandler的代理类物件,然后呼叫其中的setObj方法完成两项操作

58 //①将被代理类物件传入,执行时候呼叫的是被代理类重写的方法

59 //②返回一个类物件,通过代理类物件执行界面中的方法

60 DynamicProxyHandler dynamicProxyHandler = new DynamicProxyHandler;

61 TargetOne targetOne = (TargetOne) dynamicProxyHandler.setObj(targetOneImpl);

62 targetOne.action; //呼叫该方法执行时都会转为对DynamicProxyHandler中的invoke方法的呼叫

63 }

64 }

执行结果如下。现在我们对比jdk提供的动态代理和我们刚刚实现的静态代理,刚刚说到静态代理对于新新增的界面需要定义对应的代理类去代理界面的实现类。而上面的测试程式所使用的动态代理规避了这个问题,即我们不需要显示的指定每个界面对应的代理类,有新的界面新增没有关系,只需要在使用的时候传入界面对应的实现类然后返回代理类物件(界面实现型别),然后呼叫被代理类的方法即可。

六、动态代理与AOP简单实现

1、AOP是什么

AOP(Aspect Orient Programming)我们一般称之为面向切面程式设计,作为一种面向物件的补充,用于处理系统中分布于各个模组的横切关注点,比如事务管理、日志记录等。AOP实现的关键在于AOP的代理(实际实现上有静态代理和动态代理),我们下面使用JDK的动态代理的方式模拟实现下面的场景。

2、模拟实现AOP

我们先考虑下面图中的情况和说明。然后我们使用动态代理的思想模拟简单实现一下这个场景

1 package aop;

3 import java.lang.reflect.InvocationHandler;

4 import java.lang.reflect.Method;

5 import java.lang.reflect.Proxy;

7 //基于jdk的针对界面实现动态代理,要求的界面

8 interface Target {

9 void login;

10

11 void logout;

12 }

13

14 //被代理类

15 class TargetImpl implements Target {

16 @Override

17 public void login {

18 System.out.println("log......");

19 }

20

21 @Override

22 public void logout {

23 System.out.println("logout......");

24 }

25 }

26

27 class Util {

28 public void printLog {

29 System.out.println("我是记录打印日志功能的方法......");

30 }

31

32 public void getProperties {

33 System.out.println("我是获取配置档案资讯功能的方法......");

34 }

35 }

36

37 //实现了InvocationHandler的统一代理类

38 class DynamicProxyHandler implements InvocationHandler {

39 private Object object;

40

41 /**

42 * 引数为obj,是应对对不同的被代理类,都能系结与该代理类的代理关系

43 * 这个方法会将targetOne指向实际实现界面的子类物件,即当前代理类实际要去代理的那个类

44 */

45 public void setObj(Object obj) {

46 this.object = obj;

47 }

48

49 @Override

50 public Object invoke(Object proxy, Method method, Object args) throws Throwable {

51 Util util = new Util;

52 util.getProperties;

53 Object object = method.invoke(this.object, args); //这个方法是个动态的方法,可以是login,可以是logout,具体在测试呼叫中呼叫不同方法

54 util.printLog;

55 return object;

56 }

57 }

58

59 //该类的主要作用就是动态的建立一个代理类的物件,同时需要执行被代理类

60 class MyDynamicProxyUtil {

61 //引数obj表示动态的传递进来被代理类的物件

62 public static Object getProxyInstance(Object object) {

63 //获取代理类物件

64 DynamicProxyHandler dynamicProxyHandler = new DynamicProxyHandler;

65 dynamicProxyHandler.setObj(object);

66 //设定好代理类与被代理类之间的关系后,返回一个代理类的物件

67 return Proxy.newProxyInstance(object.getClass.getClassLoader, object.getClass.getInterfaces, dynamicProxyHandler);

68 }

69 }

70

71 public class TestAop {

72 public static void main(String args) {

73 //获得被代理类

74 Target target = new TargetImpl;

75 //通过代理类工具类,设定实际与代理类系结的被代理类,并返回一个代理类物件执行实际的方法

76 Target execute = (Target) MyDynamicProxyUtil.getProxyInstance(target);

77 execute.login;

78 execute.logout;

79 }

80 }

现在来分析一下上面的程式码,首先我们看一下下面的这个图。在图中动态代理增加的通用日志方法配置档案方法就是增加的方法,他在执行使用者实际自己开发的方法之前、之后呼叫。对应于上面的程式就是Target界面的实现类实现的login、logout方法被代理类动态的呼叫,在他们执行之前会呼叫日志模组和配置档案模组的功能。

2019-12-07 19:53:00

相关文章