一、反射
1、概述:

1)Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类中的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。简单一句话:反射技术可以对类进行解剖。
2)一个已经可以使用的应用程序,因为程序已经做好可以运行使用,不能再进行代码的加入了。而当后期我们新的功能加入程序时,该怎么做呢?常用的作法,会提供一个配置文件,来供以后实现此程序的类来扩展功能。对外提供配置文件,让后期出现的子类直接将类名字配置到配置文件中即可。该应用程序直接读取配置文件中的内容。并查找和给定名称相同的类文件。进行如下操作:

a) 加载这个类。

b)创建该类的对象。

c)调用该类中的内容。

Note:应用程序使用的类不确定时,可以通过提供配置文件,让使用者将具体的子类存储到配置文件中。然后该程序通过反射技术,对指定的类进行内容的获取。因此,反射技术大大提高了程序的扩展性。

2、定义:

反射就是把Java类中的各种成分映射成相应的java类。一个 Class 代表一个字节码,一个 Method 代表一个字节码中方法,一个 Constructor代表一个构造方法。

例如,一个Java类中用一个Class类的对象来表示,一个类中的组成部分:成员变量,方法,构造方法,包等等信息也用一个个的Java类来表示。例如:汽车是一个类,汽车中的发动机,变速箱等等也是一个个的类。表示java类的Class类显然要提供一系列的方法,来获得其中的变量,方法,构造方法,修饰符,包等信息,这些信息就是用相应类的实例对象来表示,它们是Field、Method、Contructor、Package等等。
    反射使得一个类中的每个成员都可以用相应的反射API类的一个实例对象来表示,通过调用Class类的方法可以得到这些实例对象后,得到这些实例对象后所做的事正是学习和应用反射的要点。
3、反射的基石——Class类

1)所有的类文件都有共同属性,所以可以向上抽取,把这些共性内容封装成一个类,这个类就叫Class(描述字节码文件的对象)。Class类中就包含属性有field(字段)、method(方法)、construction(构造函数)。而field中有修饰符、类型、变量名等复杂的描述内容,因此也可以将字段封装称为一个对象。用来获取类中field的内容,这个对象的描述叫Field。同理方法和构造函数也被封装成对象Method、Constructor。要想对一个类进行内容的获取,必须要先获取该字节码文件的对象,该对象是Class类型。Class类描述的信息:类的名字,类的访问属性,类所属于的包名,字段名称的列表,方法名称的列表等。每一个字节码就是class的实例对象。

2)Class和class的区别

class:Java中的类用于描述一类事物的共性,该类事物有什么属性,没有什么属性,至于这个属性的值是什么,则由此类的实例对象确定,不同的实例对象有不同的属性值。

Class:指的是Java程序中的各个Java类是属于同一类事物,都是Java程序的类,这些类称为Class,即字节码。例如人对应的是Person类,Java类对应的就是Class。

3)获取Class对象的三种方式

加载XX.class文件进内存时就被封装成了对象,该对象就是字节码文件对象。下面就是获取Class对象得三种方式:

方式一:

通过对象的getClass方法进行获取。

如:Class clazz=new Person().getClass();//Person是一个类名

Note:每次都需要具体的类和该类的对象,以及调用getClass方法。

方式二:

任何数据类型都具备着一个静态的属性class,这个属性直接获取到该类型的对应Class对象。

如:Class clazz=Person.class;//Person是一个类名

Note:比第一种较为简单,不用创建对象,不用调用getClass方法,但是还是要使用具体的类,和该类中的一个静态属性class完成。

方式三:

较为简单,只要知道类的名称即可。不需要使用该类,也不需要去调用具体的属性和行为,就可以获取到Class对象了。

如:Class clazz=Class.forName("包名.Person");//Person是一个类名

Note:这种方式仅知道类名就可以获取到该类字节码对象的方式,更有利于扩展。

拓展:

1)九个预定义的Class:
        a)包括八种基本类型(byte、short、int、long、float、double、char、boolean)的字节码对象和一种返回值为void类型的void.class。

b)Integer.TYPE是Integer类的一个常量,它代表此包装类型包装的基本类型的字节码,和int.class是相等。基本数据类型的字节码都可以用与之对应的包装类中的TYPE常量表示。

2)只要是在源程序中出现的类型都有各自的Class实例对象,如int[].class。数组类型的Class实例对象,可以用Class.isArray()方法判断是否为数组类型的。

4)Class类中的方法

static Class forName(String className)       // 返回与给定字符串名的类或接口的相关联的Class对象。

Class getClass()        //返回的是Object运行时的类,即返回Class对象即字节码对象

Constructor getConstructor()        //返回Constructor对象,它反映此Class对象所表示的类的指定公共构造方法。

Field getField(String name)        //返回一个Field对象,它表示此Class对象所代表的类或接口的指定公共成员字段。

Field[] getFields()        //返回包含某些Field对象的数组,表示所代表类中的成员字段。

Method getMethod(String name,Class… parameterTypes)   //返回一个Method对象,它表示的是此Class对象所代表的类的指定公共成员方法。

Method[] getMehtods()        //返回一个包含某些Method对象的数组,是所代表的的类中的公共成员方法。

String getName()        //以String形式返回此Class对象所表示的实体名称。

String getSuperclass()        //返回此Class所表示的类的超类的名称

boolean isArray()        //判定此Class对象是否表示一个数组

boolean isPrimitive()        //判断指定的Class对象是否是一个基本类型。

T newInstance()        //创建此Class对象所表示的类的一个新实例。

5)通过Class对象获取类实例

通过查看API我们知道,Class类是没有构造方法的,因此只能通过方法获取类实例对象。

之前我们用的已知类,创建对象的做法:

1)查找并加载XX.class文件进内存,并将该文件封装成Class对象。

2)再依据Class对象创建该类具体的实例。

3)调用构造函数对对象进行初始化。

如:Person p=new Person();

现在用Class对象来获取类实例对象的做法:

1)查找并加载指定名字的字节码文件进内存,并被封装成Class对象。

2)通过Class对象的newInstance方法创建该Class对应的类实例。

3)调用newInstance()方法会去使用该类的空参数构造函数进行初始化。

如:

String className="包名.Person";

Class clazz=Class.forName(className);

Object obj=clazz.newInstance();

示例:

//Person类
class Person {private String name;public int age;public Person(){System.out.println("Person is run");}public Person(String name,int age){this.age=age;this.name=name;}public String toString(){return name+":"+age;}
}
//示例
public class CreateClassDemo {public static void main(String[] args) throws Exception {createPersonClass();}//通过Class对象创建类实例方法public static void createPersonClass() throws Exception{//获取Person类的Class对象String className="cn.itheima.Person";Class clazz=Class.forName(className);//通过newInstance方法获取类的无参构造函数实例Person p=(Person)clazz.newInstance();}
}

4、Constructor类

1)概述:如果指定的类中没有空参数的构造函数,或者要创建的类对象需要通过指定的构造函数进行初始化。这时就不能使用Class类中的newInstance方法了。既然要通过指定的构造函数进行对象的初始化,就必须先获取这个构造函数——Constructor,而Constructor类代表某个类的构造方法。

2)获取构造方法:

1)得到这个类的所有构造方法:如得到上面示例中Person类的所有构造方法

Constructor[] cons = Class.forName(“cn.itheima.Person”).getConstructors();

2)获取某一个构造方法:

Constructor con=Person.class.getConstructor(String.class,int.class);

3)创建实例对象:

1)通常方式:Person p = new Person(“lisi”,30);

2)反射方式:Person p= (Person)con.newInstance(“lisi”,30);

Note:

1)创建实例时newInstance方法中的参数列表必须与获取Constructor的方法getConstructor方法中的参数列表一致。

2)newInstance():构造出一个实例对象,每调用一次就构造一个对象。

3)利用Constructor类来创建类实例的好处是可以指定构造函数,而Class类只能利用无参构造函数创建类实例对象。

示例:

public class ConstructorDemo{private static void main(String[] args) throws Exception {// 获取构造器时————要根据参数类型和个数来确定Constructor<String> conString = String.class.getConstructor(StringBuffer.class);// 根据构造器获取相应的对象时————也要确定参数类型和个数(必须和获取时的相同)String str = conString.newInstance(new StringBuffer("abc"));System.out.println(str.charAt(2));}
}

5、Field类

1)概述:Field类代表某个类中一个成员变量。

2)方法:

Field getField(String s);  //只能获取公有和父类中公有

Field getDeclaredField(String s);  //获取该类中任意成员变量,包括私有

setAccessible(ture);        //如果是私有字段,要先将该私有字段进行取消权限检查的能力,也称暴力访问。

set(Object obj, Object value);  //将指定对象变量上此Field对象表示的字段设置为指定的新值。

Object get(Object obj);  //返回指定对象上Field表示的字段的值。

示例:

package reflect;/** 反射点类ReflectPoint*/
public class ReflectPoint {private int x;public int y;public String str1 = "ball";public String str2 = "basketball";public String str3 = "itheima";public int getX() {return x;}public void setX(int x) {this.x = x;}public int getY() {return y;}public void setY(int y) {this.y = y;}public ReflectPoint() {super();}public ReflectPoint(int x, int y) {super();this.x = x;this.y = y;}@Overridepublic int hashCode() {final int prime = 31;int result = 1;result = prime * result + x;result = prime * result + y;return result;}@Overridepublic boolean equals(Object obj) {if (this == obj)return true;if (obj == null)return false;if (getClass() != obj.getClass())return false;ReflectPoint other = (ReflectPoint) obj;if (x != other.x)return false;if (y != other.y)return false;return true;}@Overridepublic String toString() {return "ReflectPoint [x=" + x + ", y=" + y + ", str1=" + str1+ ", str2=" + str2 + ", str3=" + str3 + "]";}
}public class FileDemo{private static void main(String[] args) throws Exception {// 对于源文件中的类成员变量public 只要用getField()方法就可获得Field对象ReflectPoint rp = new ReflectPoint(2,5);Field fieldY = rp.getClass().getField("y");System.out.println(fieldY.get(rp));// 如果成员变量为非公用,则必须要用getDeclearedField()获得Field对象Field fieldX = rp.getClass().getDeclaredField("x");// 暴力反射fieldX.setAccessible(true);System.out.println(fieldX.get(rp));}
}

6、Method类

1)概述:Method类代表某个类中的一个成员方法。调用某个对象身上的方法,要先得到方法,再针对某个对象调用。

2)方法:

Method[] getMethods();//只获取公共和父类中的方法。

Method[] getDeclaredMethods();//获取本类中包含私有。

Method   getMethod("方法名",参数.class(如果是空参可以写null));

Object invoke(Object obj ,参数);//调用方法,如果方法是静态,invoke方法中的对象参数可以为null。

如:获取某个类中的某个方法(如String str =”abc”)

1)通常方式:str.charAt(1);

2)反射方式:

Method charAtMethod =Class.forName(“java.lang.String”).getMethod(“charAt”,int.class);

charAtMethod.invoke(str,1);

示例:

public class MethodDemo{public static void main(String[] args){getAllMethod();}public static void getAllMethod() throws Exception {// 如果想要获取方法,必须先要有对象。Class clazz = Class.forName("reflect.ReflectPoint");ReflectPoint p = (ReflectPoint) clazz.newInstance();// 获取所以方法Method[] mes = clazz.getMethods();// 只获取公共的和父类中的。// mes=clazz.getDeclaredMethods();//获取本类中包含私有。for (Method me : mes) {System.out.println(me);}// 获取单个方法Method me = clazz.getMethod("toString", null);Object returnVaule = me.invoke(p, null);System.out.println(returnVaule);}
}

7、数组的反射

1)具有相同维数和元素类型的数组属于同一个类型,即具有相同的Class实例对象。数组字节码的名字:有[和数组对应类型的缩写,如int[]数组的名称为:[I

2)Object[]与String[]没有父子关系,Object与String有父子关系,所以new Object[]{“aaa”,”bb”}不能强制转换成new String[]{“aaa”,”bb”}; Object x =“abc”能强制转换成String x =“abc”。

3)如何得到某个数组中的某个元素的类型,

例:

int a = new int[3];Object[] obj=new Object[]{”ABC”,1};//无法得到某个数组的具体类型,只能得到其中某个元素的类型,

例:

Obj[0].getClass().getName()得到的是java.lang.String。

4)Array工具类用于完成对数组的反射操作。

Array.getLength(Object obj);//获取数组的长度

Array.get(Object obj,int x);//获取数组中的元素

5)基本类型的一维数组可以被当作Object类型使用,不能当作Object[]类型使用;非基本类型的一维数组,既可以当做Object类型使用,又可以当做Object[]类型使用。

示例:

import java.lang.reflect.Array;
import java.util.Arrays;public class ArrayReflect {public static void main(String[] args) {int [] a1 = new int[]{1,2,3};int [] a2 = new int[4];int[][] a3 = new int[2][3];String [] a4 = new String[]{"a","b","c"};System.out.println(a1.getClass().equals(a2.getClass()));//trueSystem.out.println(a1.getClass().equals(a3.getClass()));//falseSystem.out.println(a1.getClass().equals(a4.getClass()));//falseSystem.out.println(a1.getClass().getName());//[ISystem.out.println(a4.getClass().getName());//[Ljava.lang.String;System.out.println(a1.getClass().getSuperclass());//class java.lang.ObjectSystem.out.println(a4.getClass().getSuperclass());//class java.lang.ObjectObject obj1=a1;Object obj2=a3;Object obj3=a4;//        Object[] obj11=a1;//这样是不行的,因为a1中的元素是int类型,基本数据类型不是ObjectObject[] obj13=a3;Object[] obj14=a4;//这样可以,因为String数组中的元素属于ObjectSystem.out.println(a1);//[I@4caaf64eSystem.out.println(a4);//[Ljava.lang.String;@6c10a234System.out.println(Arrays.asList(a1));//[I@4caaf64eSystem.out.println(Arrays.asList(a4));//[a, b, c]//Array工具类用于完成对数组的反射操作。如打印任意数值printObject(a1);printObject(a4);printObject("abc");}//打印任意数值private static void printObject(Object obj) {Class clazz=obj.getClass();//如果传入的是数组,则遍历if(clazz.isArray()){int len =Array.getLength(obj);//Array工具类获取数组长度方法for(int x=0;x<len;x++){System.out.println(Array.get(obj, x));//Array工具获取数组元素}}elseSystem.out.println(obj);}
}

8、反射的应用——>实现框架功能

1)框架:通过反射调用Java类的一种方式。框架和工具类的区别:工具类被用户类调用,而框架是调用用户提供的类。

2)框架机器要解决的核心问题:在写框架(造房子的过程)的时候,调用的类(安装的门窗等)还未出现,那么框架无法知道要被调用的类名,所以在程序中无法直接new其某个类的实例对象,而要用反射来做。

3)简单框架程序的步骤:

a)创建一个配置文件如:config.properties,然后写入配置信息。如键值对:className=java.util.ArrayList,等号右边的配置键,右边是值。

b)代码实现,加载此文件:

将文件读取到读取流中,要写出配置文件的绝对路径。

如:InputStream is=new FileInputStream(“配置文件”);

用Properties类的load()方法将流中的数据存入集合。

关闭流:关闭的是读取流,因为流中的数据已经加载进内存。

c)通过getProperty()方法获取className,即配置的值,也就是某个类名。

d)用反射的方式,创建对象newInstance()。

e)执行程序主体功能

示例:

package reflect;import java.io.InputStream;
import java.util.Collection;
import java.util.Properties;public class ReflectDemo2 {public static void main(String[] args) throws Exception {// 用完整的路径,但不能是硬编码而是运算得出的// InputStream in = new FileInputStream("config.properties");// InputStream in =// ReflectDemo1.class.getClassLoader().getResourceAsStream("reflect/config.properties");InputStream in = ReflectDemo1.class.getResourceAsStream("config.properties");Properties prop = new Properties();prop.load(in);in.close();String className = prop.getProperty("className");Collection<ReflectPoint> al = (Collection<ReflectPoint>) Class.forName(className).newInstance();// Collection al = new HashSet();ReflectPoint rp1 = new ReflectPoint(3, 3);ReflectPoint rp2 = new ReflectPoint(6, 6);ReflectPoint rp3 = new ReflectPoint(3, 3);al.add(rp1);al.add(rp1);al.add(rp2);al.add(rp3);// 以下会出现内存泄漏,因为改变了rp1的y值,其hashCode也就随之改变及内存地址改变,再用remove移除时就没有成功// rp1.y = 7;// al.remove(rp1);System.out.println(al.size());}
}

二、内省
1、概述:内省就是对程序内部进行检查,了解更多的底层细节,主要针对JavaBean进行操作。
2、JavaBean:

1)JavaBean是一种特殊的Java类,主要用于传递数据信息,这种Java类中的方法主要用于访问私有的字段,且方法符合某种特殊的命名规则。

2)它是一种特殊的Java类,其中的方法符合特殊的规则。只要一个类中含有get或is和set打头的方法,就可以将其当做JavaBean使用。

3)字段和属性:JavaBean的字段就是我们定义的一些成员变量,如private String name等。JavaBean的属性是根据其中的setter和getter方法来确定的,而不是依据其中的变量,如方法名为setId,则中文意思是设置Id,getId也是如此;去掉set或者get前缀,剩余部分就是属性名称。如果剩余部分的第二个字母小写,则把剩余部分改为小写。如:getAge/setAge-->age;gettime-->time;setTime-->time;getCPU-->CPU。

总之,一个类被当作javaBean使用时,JavaBean的属性是根据方法名推断出来的,它根本看不到java类内部的成员变量。

3、作用:

如果要在两个模板之间传递多个信息,可将这些信息封装到一个JavaBean中,这种JavaBean的实例对象通常称之为值对象(Value Object,简称VO),这些信息在类中用私有字段来储存,如果读取或设置这些字段的值,则需要通过一些相应的方法来访问。

4、JavaBean的好处:一个符合JavaBean特点的类当做普通类一样可以使用,但是把它当做JavaBean类用会带来一些额外的好处:

1)在Java EE开发中,经常要使用到JavaBean。很多环境就要求按JavaBean方式进行操作,别人都这么用和要求这么做,那你就没什么挑选的余地。

2)JDK中提供了对JavaBean进行操作的API,这套API称为内省,若要自己通过getX的方式来访问私有x,可用内省这套API,操作JavaBean要比使用普通的方式更方便。

5、对JavaBean的复杂内省操作

1、在IntroSpector类中有getBeanInfo(Class cls)的方法,通过此方法获取BeanInfo实例。参数是相应对象的字节码,即Class对象。

2、BeanInfo类中有getPropertyDescriptors()的方法,可获取所有的JavaBean类中的属性信息,返回一个PropertyDescriptor[]。

3、在通过遍历的形式,获取与想要的那个属性信息。

示例:

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
//接上述反射点类ReflectPoint继续
import reflect.ReflectPoint;/** 内省:操作的是JavaBean(特殊的类)* JavaBean类是含有get()、set()方法,其中的公有方法用于访问私有成员变量,并且命名符合一定规则* 例如:getAge——>age     getCPU——>CPU* */
//接上述ReflectPoint类继续public class IntroSpectorDemo {public static void main(String[] args) throws Exception {ReflectPoint rp = new ReflectPoint(3, 4);String propertyName = "x";// "x"-->"X"-->"getX"-->MethodGetX-->// 用内省的方式// 获取并getX方法Object retval = getProperty1(rp, propertyName);System.out.println(retval);Object value = 5;// 获取并调用setX方法setProperty(rp, propertyName, value);System.out.println(rp.getX());}// 获取并调用setX方法private static void setProperty(Object rp, String propertyName, Object value)throws IntrospectionException, IllegalAccessException,InvocationTargetException {PropertyDescriptor pd = new PropertyDescriptor(propertyName,rp.getClass());// 创建对象关联Method methodSetX = pd.getWriteMethod();// 获取JavaBean类中的setX方法methodSetX.invoke(rp, value);// 调用setX方法}// 获取并调用getX方法第一中方式private static Object getProperty1(Object rp, String propertyName)throws IntrospectionException, IllegalAccessException,InvocationTargetException {PropertyDescriptor pd = new PropertyDescriptor(propertyName,rp.getClass());// 创建对象关联Method methodGetX = pd.getReadMethod();// 获取JavaBean类中的getX方法Object retval = methodGetX.invoke(rp);// 调用getX方法return retval;}// 获取调用getX方法的第二种方式public static Object getProperty(Object rp, String propertyName)throws Exception {BeanInfo beanInfo = Introspector.getBeanInfo(rp.getClass());PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();Object retVal = null;for (PropertyDescriptor pd : pds) {if (pd.getName().equals(propertyName)) {Method methodGetX = pd.getReadMethod();retVal = methodGetX.invoke(rp);break;}}return retVal;}
}

6、BeanUtils工具包

1)BeanUtils等工具包都是由阿帕奇提供的,为了便于开发。

2)BeanUtils可以将8种基本数据类型进行自动的转换,因此对于非基本数据类型,就需要注册转换器Converter,这就需要ConverUtils包。

3)好处:

1)提供的set或get方法中,传入的是字符串,返回的还是字符串,因为在浏览器中,用户输入到文本框的都是以字符串的形式发送至服务器上的,所以操作的都是字符串。也就是说这个工具包的内部有自动将整数转换为字符串的操作。

2)支持属性的级联操作,即支持属性链。如可以设置:人的脑袋上的眼睛的眼珠的颜色。这种级联属性的属性连如果自己用反射,那就很困难了,通过这个工具包就可以轻松调用。

4)可以和Map集合进行相互转换:可将属性信息通过键值对的形式作为Map集合存储(通过static java.util.Mapdescribe(java.lang.Object bean)的方法)。也可以将Map集合转换为JavaBean中的属性信息(通过static void populate(java.lang.Objectbean, java.util.Map properties)的方法)。

Note:要正常使用BeanUtils工具,还要将Apache公司的logging(日志)的jar包也添加进Build Path。在工程中导入工具jar包。两种方式:

a)右键项目--选择Properties---Java Build Path--选择Liberiers标签。AddExternal Jars--选择要导入的jar包即可。

这样做有个问题就是如果jar路径发生变化,项目就不能使用到这个jar包。

b)在项目中建立一个lib目录,专门用于存放项目所使用到的jar工具包。将要使用到jar包复制粘贴进来,并在jar上点右键--选择Builder Path---Add to BiuldPath,即可。
            这时jar包中的对象,就可以使用了。这样做项目移动,jar随项目移动。

5)BeanUtils工具包中还有一个工具类PropertyUtils,用法跟BeanUtils一样。区别:

a)BeanUtils会对JavaBean的属性的类型进行转换,如属性本身是integer,会转换为String。

b)PropertyUtils以属性本身的类型进行操作。

示例:

package cn.itheima.demo;//接前面的reflectPoint类继续
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;public class IntroSpectorDemo {public static void main(String[] args) throws Exception {ReflectPoint rp=new ReflectPoint(2,3);String propertyName="x";//"x"-->"X"-->"getX"-->MethodGetX-->//用BeanUtils工具包的方法System.out.println(BeanUtils.getProperty(hct, propertyName));//getBeanUtils.setProperty(rp, propertyName, "9");//setSystem.out.println(rp.getX());    //对于JavaBean中的属性是对象的操作BeanUtils.setProperty(rp, "birthday.time", "10");//setSystem.out.println(BeanUtils.getProperty(rp, "birthday.time"));//get}
}

三、类加载器
1、概述: 简单说,类加载器就是加载类的工具。 在java程序中用到一个类,出现了这个类的名字。java虚拟机首先将这个类的字节码加载到内存中,通常这个类的字节码的原始信息放在硬盘上的classpath指定的目录下,把.class文件的内容加载到内存里面来,再对它进行处理,处理之后的结果就是字节码。这些工作就是类加载器在操作。

2、默认类加载器:

1)Java虚拟机中可安装多个类加载器,系统默认的有三个主要的,每个类负责加载特定位置的类:BootStrap、ExtClassLoader、AppClassLoader

2)类加载器本身也是Java类,因为它是Java类的加载器,本身也需要被类加载器加载,显然必须有第一个类加载器而不是java类的,这正是BootStrap。它是嵌套在Java虚拟机内核中的,已启动即出现在虚拟机中,是用c++写的一段二进制代码。所以不能通过java程序获取其名字,获得的只能是null。

3、Java虚拟机中的所有类加载器采用父子关系的树形结构进行组织,在实例化每个类装载器对象时,需要为其指定一个父级类装载器对象或者默认采用系统类装载器为其父级类加载。
4、类加载器的继承关系和管辖范围示例图:

示例:

public class ClassLoaderDemo {public static void main(String[] args) throws Exception {System.out.println(ClassLoaderDemo.class.getClassLoader().getClass().getName());//获取当前类的类加载器---AppClassLoaderSystem.out.println(System.class.getClassLoader());//获取String类的类加载器,因为BootStrap不是Java语言编写的,所以此处返回null}
}

5、委托机制

1)每个ClassLoader本身只能分别加载特定位置和目录中的类,但它们可以委托其他的类加载器去加载类,这就是类加载器的委托模式。

2)加载类的顺序:

a)首先,当前线程的类加载器去加载线程中的第一个类。

b)若A引用类B(继承或者使用了B),Java虚拟机将使用加载类的类加载器来加载类B。

c)还可直接调用ClassLoader的LoaderClass()方法,来指定某个类加载器去加载某个类。

2)每个类加载器加载类时,又先委托给上级类加载器。类装载器一级级委托到BootStrap类加载器,当BootStrap无法加载当前所要加载的类时,然后才一级级回退到子孙类加载器去进行加载。当回退到最初的发起者类装载器时,如果它自己也不能完成类的装载,那就会抛出ClassNotFoundException异常。这时就不会再委托发起者加载器的子类去加载了,如果它还有子类的话。 简单说,就是先由发起者将类一级级委托为BootStrap,从父级开始找,找到了直接返回,没找到再助剂让其子级找,直到发起者,再没找到就报异常。

3)委托机制的优点:可以集中管理,不会产生多字节码重复的现象。

补充:(面试题)可不可以自己写个类为:java.lang.System呢?

第一:通常是不可以的,由于类加载器的委托机制,会先将System这个类一级级委托给最顶级的BootStrap,由于BootStrap在其指定的目录中加载的是rt.jar中的类,且其中有System这个类,那么就会直接加载自己目录中的,也就是Java已经定义好的System这个类,而不会加载自定义的这个System。

第二:但是还是有办法加载这个自定义的System类的,此时就不能交给上级加载了,需要用自定义的类加载器加载,这就需要有特殊的写法才能去加载这个自定义的System类的。
示例:

package reflect;public class ClassLoaderDemo {public static void main(String[] args) {ClassLoader loader=ClassLoaderDemo.class.getClassLoader();while (loader!=null) {//循环获取本类的类加载器和上级类加载器System.out.println(loader.getClass().getName());loader=loader.getParent();//将此loader的上级赋给loader}System.out.println(loader);}
}

6、自定义类加载器

1)概述:自定义的类加载器必须继承抽象类ClassLoader,要覆写其中的findClass(String name)方法,而不用覆写loadClass()方法。

2)覆写findClass(Stringname)方法的原因:

a)在loadClass()内部是会先委托给父级,当父级找不到后返回,再调用findClass(String name)方法,也就是你自定义的类加载器去找。所以只需要覆写findClass方法,就能实现用自定义的类加载器加载类的目的。因为,一般自定义类加载器,会把需要加载的类放在自己指定的目录中,而java中已有的类加载器是不知道你这个目录的,所以会找不到。这样才会调用你复写的findClass()方法,用你自定义的类加载器去指定的目录加载类。

b)这是一种模板方法设计模式。这样就保留了loadClass()方法中的流程(这个流程就是先找父级,找不到再调用自定义的类加载器),而我们只需复写findClass方法,实现局部细节就行了。ClassLoader提供了一个protected  Class<?>defineClass(String name, byte[] b, int off, int len)方法,只需要将类对应的class文件传入,就可以将其变为字节码。

3)编程步骤:

a)编写一个对文件内容进行简单加密的程序

b)编写好了一个自己的类加载器,可实现对加密过来的类进行加载和解密。

c)编写一个程序,调用类加载器加载类,在源程序中不能用该类名定义引用变量,因为编译器无法识别这个类,程序中除了可使用ClassLoader的loadClass方法外,还可以使用设置线程的上下文类加载器或系统类加载器,然后再使用Class.forName。

4)操作步骤:

a)对不带包名的class文件进行加密,加密结果存放到另外一个目录,例如: java MyClassLoader MyTest.class F:\itcast

b)运行加载类的程序,结果能够被正常加载,但打印出来的类装载器名称为AppClassLoader:java MyClassLoader MyTest F:\itcast

c)用加密后的类文件替换CLASSPATH环境下的类文件,再执行上一步操作就出问题了,错误说明是AppClassLoader类装载器装载失败。

d)删除CLASSPATH环境下的类文件,再执行上一步操作就没问题了。

示例:

package classloader;import java.util.Date;
//定义一个测试类,继承Date,便于使用时加载
public class ClassLoaderAttachment extends Date{//复写toString方法public String toString(){return "Hello HeiMa!";}
}import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;public class MyClassLoader extends ClassLoader{public static void main(String[] args) throws Exception {String srcPath=args[0];//文件源String destDir=args[1];//文件目的InputStream ips=new FileInputStream(srcPath);String destFileName=srcPath.substring(srcPath.lastIndexOf("\\")+1);String destFilePath=destDir+"\\"+destFileName;OutputStream ops=new FileOutputStream(destFilePath);cypher(ips,ops);//加密class字节码ips.close();ops.close();}//加密、解密方法private static void cypher(InputStream ips,OutputStream ops) throws Exception{int b=-1;while((b=ips.read())!=-1){ops.write(b^0xff);}}@Override//覆盖ClassLoader的findClass方法protected Class<?> findClass(String name) throws ClassNotFoundException {name=name.substring(name.lastIndexOf(".")+1);String classFileName=classDir+"\\"+name+".class";//获取class文件名InputStream ips=null;try {ips=new FileInputStream(classFileName);ByteArrayOutputStream bos=new ByteArrayOutputStream();//定义字节数组流cypher(ips,bos);//解密ips.close();byte[] buf=bos.toByteArray();//取出字节数组流中的数据return defineClass(null, buf,0,buf.length);//加载进内存} catch (Exception e) {e.printStackTrace();}return null;}private String classDir;public MyClassLoader(){}//带参数的构造函数public MyClassLoader(String classDir){this.classDir=classDir;}}import java.util.Date;public class ClassLoaderDemo {public static void main(String[] args) throws Exception {//将用自定义的类加载器加载.class文件Class clazz=new MyClassLoader("itheimalib").loadClass("cn.itheima.demo.ClassLoaderAttachment");Date d1 =  (Date)clazz.newInstance();//获取Class类的实例对象,因为使用ClassLoaderAttachment,编译器会检测出错误,所以用父类DateSystem.out.println(d1);}
}

Note:类加载器加载配置文件的方式:
    第一种:
        InputStream ips=ReflectDemo2.class.getClassLoader().getResourceAsStream(“blog/config.properties”);
    第二种:
        InputStream ips=ReflectDemo2.class.getResourceAsStream(“config.properties”);
四、代理

1、概述:生活中的代理,就是常说的代理商,从厂商将商品卖给消费者,消费者不用很麻烦的跑到厂商那里去购买。而程序中的代理,要为已经存在的多个具有相同接口的目标类的各个方法增加一些系统功能,如异常处理、日志、计算方法的运行时间、事物管理等等。
2、作用:代理为已存在的多个具有相同接口的目标类的各个方法增加一些系统功能。
(例如: 编写 一个与目标类具有相同接口的代理类, 代理类的每个方法调用目标类的相同方法,并在 调用方法时加上系统功能的代码。 )
3、代理架构图

示例:

// 接口
interface A {void sayHello();
}// 目标类
class X implements A {@Overridepublic void sayHello() {System.out.println("sayHello");}
}// 代理类
class Y implements A {@Overridepublic void sayHello() {long startTime = System.currentTimeMillis();//获取系统时间--程序开始点System.out.println("sayHello");long endTime = System.currentTimeMillis();//程序结束点}
}

目标类和代理类通常实现同一个或多个接口,一般用该接口来引用其子类(代理类),如:

Collection  coll = new ArrayList();

4、代理类的优点:

如果采用工厂模式和配置文件的方式进行管理,则不需要修改客户端程序,在配置文件中配置是使用目标类还是代理类。这样以后很容易切换,如果想要日志功能时,就配置代理类,否则配置目标类,这样,增加系统功能很容易,以后运行一段时间后,又想换掉系统功能也很容易。
5、AOP

1)概述:AOP(AspectOriented Program)即面向方面的编程。

2)示意图:(系统中存在交叉业务,一个交叉业务就是要切入到系统中的一个方面)

安全      事务         日志

StudentService ------|----------|------------|-------------

CourseService  ------|----------|------------|-------------

MiscService    -------|----------|------------|-------------

Note:安全、事务、日志等功能要贯穿于好多个模块中,所以他们就是交叉业务。

3)用具体的程序代码描述交叉业务:

1)交叉业务的代码实现

method1         method2          method3

{                     {                     {

------------------------------------------------------切面

....                    ....                  ......

------------------------------------------------------切面

}                     }                      }

2)交叉业务的编程问题即为面向方面的编程(Aspect orientedprogram ,简称AOP),AOP的目标就是要使交叉业务模块化。可以采用将切面代码移动到原始方法的周围,这与直接在方法中编写切面代码的运行效果是一样的,如下所示:

------------------------------------------------------切面

func1         func2            func3

{                {                   {

....               ....                  ......

}                }                    }

------------------------------------------------------切面

Note:因此使用代理技术正好可以解决这种问题,代理是实现AOP功能的核心和关键技术,只要是用到面向方面的编程,就涉及到代理,类似于模板方法。

6、动态代理

1)由来:要为系统中的各种接口的类增加代理功能,那将需要太多的代理类,这时就不能全部采用静态代理的方式,因为要写成百上千个代理类是非常麻烦的,这时就需用动态代理技术。

2)定义:JVM可以在运行期,动态生成类的字节码,这种动态生成的类(不是代理,只是拿来用作代理类)往往被用作代理类,即动态代理类。

Note:JVM生成的动态类必须实现一或多个接口,所以JVM生成的动态代理类只能用作具有相同接口的目标类代理。

3)CGLIB库可以动态生成一个类的子类,一个类的子类也可以用作该类的代理。所以,如果要为一个没有实现接口的类生成动态代理类,那么可以使用CGLIB库。

4)代理类各个方法通常除了调用目标相应方法和对外返回目标返回的结果外,还可以在代理方法中的如下位置上加上系统功能代码:

a)在调用目标方法之前

b)在调用目标方法之后

c)在调用目标方法前后

d)在处理目标方法异常的catch块中。

5)分析JVM动态生成的类

以创建实现了Collection接口的代理类为例:

a)用Proxy.getProxyClass方法创建实现了Collection接口的动态类和查看其名称,分析getProxyClass方法的各个参数。

1)编码列出动态类中的所有构造方法和参数签名

2)编码列出动态类中的所有方法和参数签名

b)创建动态类的实例对象

1)用反射获得构造方法

2)编写一个最简单的InvocationHandler类(代理类构造方法接收的参数)

3)调用构造方法创建动态类的实例对象,并将编写的InvocationHandler类的实例对象传进去

4)打印创建的对象和调用对象的没有返回值的方法和getClass方法,演示调用其他有返回值的方法报告了异常。

5)将创建动态类的实例对象的代理改成匿名内部类的形式编写,锻炼匿名内部类的使用。

c)也可以直接用Proxy.newInstance方法直接一步就创建出代理对象。

d)让JVM创建动态类及其实例对象,需要提供的信息:

1)生成类中的哪些方法,通过让其实现哪些接口的方式进行告知。

2)产生的类字节码必须有一个关联的类加载器对象

3)生成的类中的方法的代码是怎么样的,也得由我们自己提供。把我们的代码写在一个约定好的子接口对象的方法中,把对象传给它,它调用我们的方法,即相当于插入了我们自己的代码。提供执行代码的对象就是InvocationHandler对象,它是在创建动态类的实例对象的构造方法时传递进去的,在上面的InvocationHandler对象的invoke方法中,加一点代码就可以看到这些代码被调用运行了。

6)总结分析动态代理类的统计原理和结构

1)怎样将目标传进去:

a)直接在InvocationHandler实现类中创建目标类的实例对象,可看运行效果和加入日志代码,但是没有实际意义。

b)为InvocationHandler实现类注入目标的实例对象,不能采用匿名内部类的形式了。

c)让匿名内部类的InvocationHandler实现类访问外面的方法中的目标类实例对象的final类型的引用变量。

2)动态代理的工作原理:

a)Client(客户端)调用代理,代理的构造方法接收一个InvocationHandler,client调用代理的各个方法,代理的各个方法请求转发给刚才通过构造方法传入的handler对象,又把各请求分发给目标的相应的方法。

示意图:

b)将创建代理的过程改为一种更优雅的方式,eclipse重构出一个getProxy方法绑定接收目标,同时返回代理对象,让调用者更懒惰,更方便,调用者甚至不用接触任何代理的API。在这里将InvocationHandler加入到Proxy的构造方法中,因此,在创建出来的对象,就会存有构造方法中InvocationHandler的一些功能和信息,因为我们把想要运行的代码封装在InvocationHandler对象,把它传入到构造函数中,那么就实现了代理对象每次调用与目标方法相同方法(因为实现了同一接口)时,都会调用我们加入到InvocationHandler对象中的代码。这就保证了每次调用代理时,可以在目标上加入我们自己加入的功能。

3)把系统功能代理模块化,即切面代码也改为通过参数形式提供,怎么把要执行的系统功能代码以参数的形式提供:

a)把要执行的代码装到一个对象的某个方法中,然后把此对象作为参数传递,接收者只要调用这个对象的方法,即等于执行了外接提供的代码。

b)为getProxy方法增加一个Advice参数。

示例:

package proxy;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collection;public class ProxyDemo {public static void main(String[] args) throws Exception{final ArrayList target=new ArrayList();//指定目标Advice advice=new MyAdvice();//将要添加的代码封装成对象//使用Proxy提供的静态newProxyInstance方法来一步到位的创建代理类实例对象Collection proxy3 = (Collection)getProxy(target,advice);proxy3.add("it");proxy3.add("hei");proxy3.add("ma");System.out.println(proxy3.size());//3}//作为一个通用的方法,就使用Object  //传入一个目标,并传入一个接口,此接口作为通信的契约,才能调用额外的方法private static Object getProxy(final Object target,final Advice advice) {Object proxy3=Proxy.newProxyInstance(//第一个参数,定义代理类的类加载器target.getClass().getClassLoader(),//第二个参数,代理类要实现的接口列表,这里要与target实现相同的接口//new Class[]{Collection.class},target.getClass().getInterfaces(),//第三个参数,代理类的构造函数的参数new InvocationHandler() {@Override//复写invoke方法public Object invoke(Object proxy, Method method, Object[] args)throws Throwable {//使用约定的对象中的方法advice.beforeMehod();Object retval=method.invoke(target, args);//调用目标advice.afterMethod(method);return retval;   }});return proxy3;}
}import java.lang.reflect.Method;
//只要实现Advice中的方法,里面的代码功能可以随便定义,调用代理时就会被使用
public class MyAdvice implements Advice {long startTime;@Overridepublic void beforeMehod() {System.out.println("学习开始。。。");startTime=System.currentTimeMillis();}@Overridepublic void afterMethod(Method method) {System.out.println("学习结束...");long endTime=System.currentTimeMillis();System.out.println(method.getName()+"  running time:"+(endTime-startTime));}
}import java.lang.reflect.Method;//这里用两个作为示例,创建Advice接口
public interface Advice {void beforeMehod();void afterMethod(Method method);
}

7、实现AOP功能的封装与配置

工厂类BeanFactory

1)工厂类BeanFactory负责创建目标类或代理类的实例对象,并通过配置文件实现切换。

2)getBean方法根据参数字符串返回一个相应的实例对象,如果参数字符串在配置文件中对应的类名不是ProxyFactoryBean,则直接返回该类的实例对象,否则返回该类示例对象的getProxy方法返回的对象。

3)BeanFactory的构造方法接收代表配置文件的输入流对象的配置文件格式如下:

#xxx=java.util.ArrayList     (#标识注释此行)

xxx=proxy.aopframework.ProxyFactoryBean

xxx.advice=proxy.MyAdvice

xxx.target=java.util.ArrayList

4)ProxyFactoryBean充当封装成动态的工厂,需为工厂提供的配置参数信息包括:目标(target)  通知(advice)

5)BeanFactory和ProxyFactoryBean:

a)BeanFactory是一个纯粹的bean工程,就是创建bean即相应的对象的工厂。

b)ProxyfactoryBean是BeanFactory中的一个特殊的Bean,是创建代理的工厂。

实现类似spring的可配置的AOP框架的思路

1)创建BeanFactory类:

a)构造方法:接受一个配置文件,通过Properties对象加载InputStream流对象获得。

b)创建getBean(String name)方法,接收Bean的名字,从上面加载后的对象获得。

c)通过其字节码对象创建实例对象bean。

d)判断bean是否是特殊的Bean即ProxyFactoryBean,如果是就要创建代理类,并设置目标和通告,分别得到各自的实例对象,并返回代理类实例对象。否则返回普通类的实例对象。

2)创建ProxyFactoryBean(接口),此处直接定义为类做测试,其中有一个getProxy方法,用于获得代理类对象。

3)编写实现Advice接口的类和在配置文件中进行配置。
4)定义一个测试类:AopFrameworkTest,也称客户端,调用BeanFactory获取对象。

示例:

package aopframework;import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.Properties;/** 配置文件config.properties中的信息如下:xxx=java.util.ArrayListxxx=aopframework.ProxyFactoryBeanxxx.advice=proxy.MyAdvicexxx.target=java.util.ArrayList*///两个示例方法,封装在Advice接口中
interface Advice {public void beforeMethod(Method method);public void afterMethod(Method method);
}// 创建BeanFactory类,用于创建目标类或者代理类的实例对象。
class BeanFactory {// 定义properties集合用Properties props = new Properties();public BeanFactory(InputStream in) {try {props.load(in);} catch (IOException e) {e.printStackTrace();}}// 从配置文件中获取对象public Object getBean(String name) {String className = props.getProperty(name);// 获取配置文件中的值Class<?> clazz = null;Object bean = null;try {clazz = Class.forName(className);// 通过反射创建对象bean = clazz.newInstance();} catch (Exception e) {e.printStackTrace();}// 如果创建的对象是ProxyFactoryBean类型,则通过getProxy方法获取代理类对象if (bean instanceof ProxyFactoryBean) {ProxyFactoryBean proxyFactoryBean = (ProxyFactoryBean) bean;Advice advice = null;Object target = null;try {// 从配置文件中获取代理类额外添加的代码封装成的对象advice = (Advice) Class.forName(props.getProperty(name + ".advice")).newInstance();// 从配置文件中获取目标target = Class.forName(props.getProperty(name + ".target")).newInstance();} catch (Exception e) {e.printStackTrace();}proxyFactoryBean.setAdvice(advice);proxyFactoryBean.setTarget(target);// 调用getProxy方法,获取代理对象Object proxy = (proxyFactoryBean).getProxy();return proxy;}return bean;}
}// 创建ProxyFactoryBean类,用于产生代理类实例对象
class ProxyFactoryBean {private Advice advice;private Object target;public Advice getAdvice() {return advice;}public void setAdvice(Advice advice) {this.advice = advice;}public Object getTarget() {return target;}public void setTarget(Object target) {this.target = target;}Object getProxy() {// 第一个参数target.getClass().getClassLoader(),定义代理类的类加载器// 第二个参数target.getClass().getInterfaces(),代理类要实现的接口列表,这里要与target实现相同的接口// 第三个参数new InvocationHandler() {...},代理类的构造函数的参数Object proxy3 = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),new InvocationHandler() {@Override// 复写invoke方法public Object invoke(Object proxy, Method method,Object[] args) throws Throwable {// 使用约定的对象中的方法advice.beforeMethod(method);// 调用目标Object retVal = method.invoke(target, args);advice.afterMethod(method);return retVal;}});return proxy3;};
}public class AopFrameworkTest {public static void main(String[] args) {InputStream in = AopFrameworkTest.class.getResourceAsStream("config.properties");Object bean = new BeanFactory(in).getBean("xxx");System.out.println(bean.getClass().getName());((Collection<?>) bean).clear();}}

至此,高新技术阶段的主要知识:反射、内省、代理基本讲说完毕,由于和后期学习Java EE关系密切,所以知识点很重要。
     以上所述仅代表个人观点,如有出入请谅解。

高新技术(反射、内省、类加载器、代理)相关推荐

  1. 反射,类加载器,动态代理

    反射技术:其实就是动态加载一个指定的类,并获取该类中的所有的内容.即 将字节 码文件(class文件)封装成对象(Class对象),并将字节码文件(class文件所对应的类)中的内容(方法和属性)都封 ...

  2. 反射setaccessible_advancedday16类加载器,反射

    加载配置文件 在src目录下创建一个config.properties配置文件,使用类加载器加载配置文件. //创建集合Properties properties=new Properties();/ ...

  3. 反射-注解-类加载器知识

    反射知识 概念 反射机制就是将类的各个组成部分(属性,方法,构造器)封装为其他对象 Class Method Constructor 相关的核心类 java.lang.Class(这个就是类的对象) ...

  4. 深入理解Java类加载器:Java类加载原理解析

    http://blog.csdn.net/zhoudaxia/article/details/35824249 1 基本信息 每个开发人员对java.lang.ClassNotFoundExcetpi ...

  5. 细说JVM(类加载器)

    一.类加载器的基本概念 顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中.一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java ...

  6. 使用反射代理类加载器的潜在内存使用问题

    问题(摘要) 大量的类加载器 sun/reflect/DelegatingClassLoader,用来加载sun/reflect/GeneratedMethodAccessor类,可能导致潜在的占用大 ...

  7. 黑马程序员-java高新技术-代理和类加载器

    ------- android培训. java培训.期待与您交流! ---------- 代理模式:为其他对象提供一种代理以控制对这个对象的访问.说白了就是,在一些情况下客户不想或不能直接引一个对象, ...

  8. JAVA基础加强(张孝祥)_类加载器、分析代理类的作用与原理及AOP概念、分析JVM动态生成的类、实现类似Spring的可配置的AOP框架...

    1.类加载器 ·简要介绍什么是类加载器,和类加载器的作用 ·Java虚拟机中可以安装多个类加载器,系统默认三个主要类加载器,每个类负责加载特定位置的类:BootStrap,ExtClassLoader ...

  9. day19_java基础加强_动态代理+注解+类加载器

    一.动态代理 1.1.代理模式     什么是代理模式及其作用?         Proxy Pattern(即:代理模式),23种常用的面向对象软件的设计模式之一.         代理模式的定义: ...

最新文章

  1. java string改变的影响_为什么Java的string类要设成immutable(不可变的)
  2. mysql修改数据库字符集,编码
  3. LZMA demo挑选使用备忘
  4. 面试 4 个月,最终入职大厂经验分享!
  5. c# mongodb or查询_C# 查询MongoDB中的数据
  6. Python三目运算符
  7. ds1302模块 树莓派_树莓派用4g模块实现三网通开机自启动
  8. 计算SharePoint两个日期和时间字段之间的时间差值
  9. 2016 Multi-University Training Contests
  10. v4l2API无法执行VIDIOC_DQBUF的问题
  11. [原创]java导出excel
  12. 思普linux安装教程,思普产品全生命周期管理系统_全生命周期管理_云市场-华为云...
  13. 找网络高手联系方式_怎么才能联系到网络高手(找网络高手联系方式)
  14. PE文件格式详解(0)
  15. 《Java程序员,上班那点事儿》荣登北京新华书店销售榜第2名
  16. 搭建web服务端网络共享及实时备份(只能用堡垒机连接)
  17. Tableau 桑基图
  18. 华科 计算机学院 何琨,华中科技大学
  19. 安卓手机内置NFC模块的使用和开发
  20. 2022年IT服务行业研究报告

热门文章

  1. 中国再生金属行业占有率分析与营销战略研究报告2022-2028年
  2. sql语句给表添加datetime类型列,并设置默认值
  3. 2023离职与调薪调研报告
  4. 中华活页文选杂志中华活页文选杂志社中华活页文选编辑部2022年第11期目录
  5. python 图像与三维数组
  6. JS本地存储方式,结合案例一看就懂
  7. 如何限制计算机游戏,Win7如何利用映像劫持限制电脑玩游戏?
  8. 2020年蜀山区信息学竞赛试卷(小学组)
  9. 在win10 下建立虚拟局域网
  10. 北京好吃又便宜的地方