黑马程序员—【Java高新技术】之内省、注解、类加载器

时间:2023-02-17 17:14:56
------- android培训java培训、期待与您交流! ---------     这篇我们分享和总结Java高新技术之内省、注解及类加载器。

一、内省

1、概述

<span style="font-size:14px;">/**
(1)IntroSpector:即内省,是对内部进行检查,了解更多的底层细节。
(2)内省的作用:主要针对JavaBean进行操作。
*/</span>

2、JavaBean

/**
(1)简述
1)JavaBean是一种特殊的Java类,主要用于传递数据信息,这种Java类中的方法主要用于访问私有的字段,且方法符合某种特殊的命名规则。
2)它是一种特殊的Java类,其中的方法符合特殊的规则。只要一个类中含有get或is和set打头的方法,就可以将其当做JavaBean使用。
3)字段和属性:
字段就是我们定义的一些成员变量,如private String name;等
JavaBean的属性是根据其中的setter和getter方法来确定的,而不是依据其中的变量,如方法名为setId,则中文意思是设置Id,getId也是如此;去掉set或者get前缀,剩余部分就是属性名称。如果剩余部分的第二个字母小写,则把剩余部分改为小写。如:getAge/setAge-->age;gettime-->time;setTime-->time;getCPU-->CPU。
总之,一个类被当作javaBean使用时,JavaBean的属性是根据方法名推断出来的,它根本看不到java类内部的成员变量。
(2)作用
如果要在两个模板之间传递多个信息,可将这些信息封装到一个JavaBean中,这种JavaBean的实例对象通常称之为值对象(Value Object,简称VO),这些信息在类中用私有字段来储存,如果读取或设置这些字段的值,则需要通过一些相应的方法来访问。
(3)JavaBean的好处
一个符合JavaBean特点的类当做普通类一样可以使用,但是把它当做JavaBean类用会带来一些额外的好处:
1)在Java EE开发中,经常要使用到JavaBean。很多环境就要求按JavaBean方式进行操作,别人都这么用和要求这么做,那你就没什么挑选的余地!
2)JDK中提供了对JavaBean进行操作的API,这套API称为内省,若要自己通过getX的方式来访问私有x,可用内省这套API,操作JavaBean要比使用普通的方式更方便。
*/
    示例:
package cn.itcast.day1;  
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class IntroSpectorTest
{
public static void main(String[] args) throws Exception
{
ReflectPoint pt1 =new ReflectPoint(3,5);
String propertyName ="x";
//"x"-->"X"-->"getX"-->MethodGetX-->
Object retVal = getProperty(pt1, propertyName);
System.out.println(retVal);
Object value =7;
setProperty(pt1, propertyName, value);//调用设置函数
System.out.println(pt1.getX());
}
//封装设置函数
private static void setProperty(ReflectPoint pt1, String propertyName,Object value)throws
IntrospectionException,IllegalAccessException, InvocationTargetException
{
PropertyDescriptor pd2 = new PropertyDescriptor(propertyName,pt1.getClass());建一个符合JavaBean的存储器,第一个参数为字段,第二个参数为字节码对象
Method methodSetX=pd2.getWriteMethod();//获取带set属性字段的方法
methodSetX.invoke(pt1,value);//用反射获取set方法的值
}
//封装获取函数
private static Object getProperty(ReflectPoint pt1, String propertyName)throws
IntrospectionException, IllegalAccessException,InvocationTargetException
{
PropertyDescriptor pd = new PropertyDescriptor(propertyName,pt1.getClass());//建一个符合JavaBean的存储器,第一个参数为字段,第二个参数为字节码对象
Method methodGetX=pd.getReadMethod();//获取带get属性字段的方法
Object retVal =methodGetX.invoke(pt1);//用反射获取get方法的值
return retVal;
}
}

3、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)的方法)。
注意事项:要正常使用BeanUtils工具,还要将Apache公司的logging(日志)的jar包也添加进Build Path。
Eclipse小知识:
在工程中导入工具jar包。
两种方式:
(1)右键项目--选择Properties---Java Build Path--选择Liberiers标签。AddExternal Jars--选择要导入的jar包。即可。
这样做有个问题就是如果jar路径发生变化。项目就不能使用到这个jar包。
(2)在项目中建立一个lib目录,专门用于存放项目所使用到的jar工具包。将要使用到jar包复制粘贴进来,并在jar上点右键--选择Builder Path---Add to BiuldPath,即可。这时jar包中的对象,就可以使用了。
这样做的好处:项目移动,jar随项目移动。
*/
    示例:
package cn.itheima.demo;  

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;

public class IntroSpectorDemo {
public static void main(String[] args) throws Exception {
HashCodeTest hct=new HashCodeTest(2,3);
String propertyName="x";
//"x"-->"X"-->"getX"-->MethodGetX-->

//用BeanUtils工具包的方法
System.out.println(BeanUtils.getProperty(hct, propertyName));//get
BeanUtils.setProperty(hct, propertyName, "9");//set

System.out.println(hct.getX());

//对于JavaBean中的属性是对象的操作
BeanUtils.setProperty(hct, "birthday.time", "10");//set
System.out.println(BeanUtils.getProperty(hct, "birthday.time"));//get
}
}

二、注解

1、概述

/**
注解相当于一种标记,在程序中加了注解就等于为程序打上了某种标记,没加,则等于没有某种标记,以后,javac编译器,开发工具和其他程序可以用反射来了解你的类及各种元素上有无何种标记,看你有什么标记,就去干相应的事。标记可以加在包,类,字段,方法,方法的参数以及局部变量上。
*/

2、应用

/**
@SuppressWarnings:取消显示指定的编译器警告,比如用了过时的方法
@Override:表示重写父类的方法,若没有,则编译器会生成一条错误的消息
@Deprecated:为某个方法注解过时了
*/
    示例:
public class AnnotationTest  
{
@SuppressWarnings("deprecation")//取消警告信息
public static viod main(String[] args)
{
System.runFinalezersOnExit(true);
}
@Deprecated//备注方法已过时
public static void sayHello()
{
System.out.println("hi,传智播客");
}
@Override//表示重写,若没有则提示
public boolean equals(Object obj)
{

}
}

3、添加基本属性

/**
(1)属性:一个注解相当于一个胸牌,但仅通过胸牌还不足以区别带胸牌的两个人,这时就需要给胸牌增加一个属性来区分,如颜色等。
(2)定义格式:同接口中的方法一样:String color();
定义缺省格式:String value() default “heima”;
(3)应用:直接在注解的括号中添加自身的属性,如:
@MyAnnotation(color=”red”)
1)如果注解中有一个名称为value的属性,且你只想设置value属性(即其他属性都采用默认值或者你只有一个value属性),那么可以省略value=部分,例如:@SuppressWarnings("deprecation")。
2)可以为属性值指定缺省值(default),应用时同样可以重新设置属性值。
3)用反射方式获得注解对应的实例对象后,可以通过该对象调用属性对应的方法来获取属性值。
*/

4、添加高级属性

(1)可以为注解增加的高级属性的返回值类型有:
1)八种基本数据类型
2)String类型
3)Class类型
4)枚举类型
5)注解类型
6)前五种类型的数组
(2)数组类型的属性:
如:int[]arrayArr() default {1,2,3};//可不定义默认值
应用:@MyAnnotation(arrayArr={2,3,4}) //可重新赋值
注:若数组属性中只有一个元素(或重新赋值为一个元素),这时属性值部分可省略大括号。
(3)枚举类型的属性:
假设定义了一个枚举类TrafficLamp,它是EnumTest的内部类,其值是交通灯的三色。
定义:EnumTest.TrafficLamplamp();
应用:@MyAnnotation(lamp=EnumTestTrafficLamp.GREEN)
(4)注解类型的属性:
假定有个注解类:MetaAnnotation,其中定义了一个属性:String value()
定义:MetaAnnotation annotation() default @MetaAnnotation(”xxx”);
应用:@MyAnnotation(annotation=@MetaAnnotation(”yyy”))//重新赋值
可以认为上面这个@MyAnnotation是MyAnnotaion类的一个实例对象,同样的道理,可以认为上面这个@MetaAnnotation是MetaAnnotation类的一个实例对象,调用代码如下:
MetaAnnotationma =MyAnnotation.annotation();
System.out.println(ma.value());
(5)Class类型的属性:
定义:Class cls();
应用:@MyAnnotation(cls=AnnotationDemo.class)
注:这里的.class必须是已定义的类,或是已有的字节码对象
(6)注解的详细语法可通过查看java语言规范了解即javaLanguage Specification
*/
    示例:
package cn.itheima.demo;  

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.METHOD,ElementType.TYPE})//元注解,指定使用范围
//注解类
public @interface MyAnnotation {
String color() default "red" ;
String value();
//数组
int[] arr() default {1,2,3};
//枚举
EnumTest.TrafficLamp lamp() default EnumTest.TrafficLamp.GREEN;
//注解类
MetaAnnotation annotation() default @MetaAnnotation("heima");
//Class类
Class clazz() default System.class;
}

import java.lang.reflect.Method;

//注解类的应用,给属性赋值或者重新赋值
@MyAnnotation(lamp=EnumTest.TrafficLamp.YELLOW,value="heima",
clazz=AnnotationDemo.class,annotation=@MetaAnnotation("itheima"))
//应用类
public class AnnotationDemo {
@SuppressWarnings("deprecation")//此注解用于抑制过时信息的提示
@MyAnnotation("Method")//自定义注解应用在方法上
public static void main(String[] args) throws NoSuchMethodException, SecurityException {

System.runFinalizersOnExit(true); //这是一个过时了的方法 ,如果没有注解就会有警告提示
//判断此类是否有MyAnnotation注解
if (AnnotationDemo.class.isAnnotationPresent(MyAnnotation.class)) {
//如果有,则获取该注解
MyAnnotation annotation =AnnotationDemo.class.getAnnotation(MyAnnotation.class);
System.out.println(annotation);//@cn.itheima.Demo.MyAnnotation()
System.out.println(annotation.color());//red
System.out.println(annotation.value());//heima
System.out.println(annotation.arr().length);//3
System.out.println(annotation.lamp());//YEllOW
System.out.println(annotation.annotation().value());//itheima
System.out.println(annotation.clazz());//class cn.itheima.demo.AnnotationDemo
}

//获取方法上的注解
Method mainMethod=AnnotationDemo.class.getMethod("main",String[].class);
MyAnnotation annotationMethod=(MyAnnotation) mainMethod.getAnnotation(MetaAnnotation.class);
SuppressWarnings sw=mainMethod.getAnnotation(SuppressWarnings.class);
System.out.println(sw);//null
System.out.println(annotationMethod);//null
}
}

三、类加载器

1、概述

(1)定义:简单说,类加载器就是加载类的工具。
在java程序中用到一个类,出现了这个类的名字。java虚拟机首先将这个类的字节码加载到内存中,通常这个类的字节码的原始信息放在硬盘上的classpath指定的目录下,把.class文件的内容加载到内存里面来,再对它进行处理,处理之后的结果就是字节码。这些工作就是类加载器在操作。
(2)类加载器作用:将.class文件中的内容变为字节码加载进内存。
(3)默认类加载器:
1)Java虚拟机中可安装多个类加载器,系统默认的有三个主要的,每个类负责加载特定位置的类:BootStrap、ExtClassLoader、AppClassLoader
2)类加载器本身也是Java类,因为它是Java类的加载器,本身也需要被类加载器加载,显然必须有第一个类加载器而不是java类的,这正是BootStrap。它是嵌套在Java虚拟机内核中的,已启动即出现在虚拟机中,是用c++写的一段二进制代码。所以不能通过java程序获取其名字,获得的只能是null。
(4)Java虚拟机中的所有类加载器采用父子关系的树形结构进行组织,在实例化每个类装载器对象时,需要为其指定一个父级类装载器对象或者默认采用系统类装载器为其父级类加载。
*/
    图例说明:
黑马程序员—【Java高新技术】之内省、注解、类加载器     示例:
package cn.itheima.demo;  

public class ClassLoaderDemo {

public static void main(String[] args) {
System.out.println(
ClassLoaderDemo.class.getClassLoader().getClass().getName()
);//sun.misc.Launcher$AppClassLoader,表示由AppClassLoader加载
System.out.println(System.class.getClassLoader());//null,表示System这个类时由RootStrap加载的
}
}

2、类加载器的委托机制

(1)每个ClassLoader本身只能分别加载特定位置和目录中的类,但它们可以委托其他的类加载器去加载类,这就是类加载器的委托模式。
(2)加载类的方式
当Java虚拟机要加载一个类时,到底要用哪个类加载器加载呢?
1)首先,当前线程的类加载器去加载线程中的第一个类。
2)若A引用类B(继承或者使用了B),Java虚拟机将使用加载类的类加载器来加载类B。
3)还可直接调用ClassLoader的LoaderClass()方法,来指定某个类加载器去加载某个类。
(3)每个类加载器加载类时,又先委托给上级类加载器。
类装载器一级级委托到BootStrap类加载器,当BootStrap无法加载当前所要加载的类时,然后才一级级回退到子孙类加载器去进行加载。当回退到最初的发起者类装载器时,如果它自己也不能完成类的装载,那就会抛出ClassNotFoundException异常。这时就不会再委托发起者加载器的子类去加载了,如果它还有子类的话。
简单说,就是先由发起者将类一级级委托为BootStrap,从父级开始找,找到了直接返回,没找到再助剂让其子级找,直到发起者,再没找到就报异常。
(4)委托机制的优点:可以集中管理,不会产生多字节码重复的现象。
补充:面试题
可不可以自己写个类为:java.lang.System呢?
回答:第一、通常是不可以的,由于类加载器的委托机制,会先将System这个类一级级委托给最*的BootStrap,由于BootStrap在其指定的目录中加载的是rt.jar中的类,且其中有System这个类,那么就会直接加载自己目录中的,也就是Java已经定义好的System这个类,而不会加载自定义的这个System。
第二、但是还是有办法加载这个自定义的System类的,此时就不能交给上级加载了,需要用自定义的类加载器加载,这就需要有特殊的写法才能去加载这个自定义的System类的。
*/
    示例:
package cn.itheima.demo;  

public class ClassLoaderDemo {

public static void main(String[] args) {
/*
* 用eclipse的打包工具将ClassLoaderTest输出成jre/lib/ext目录下的itheima.jar包
* 此时再在eclipse中运行这个类时,下面代码的while循环内的运行结果显示为ExtClassLoadr。
* 这就表示,AppClassLoader在加载这个类时,会先委托给其上一级ExtClassLoader加载器去加载,而上级又委托上级
* 但是ExtClassloader的上级没有找到要加载的类,就回到ExtClassLoader,此时它在jre/lib/ext中找到了,所以就结果就显示它了。
* */
ClassLoader loader=ClassLoaderDemo.class.getClassLoader();
while (loader!=null) {
System.out.println(loader.getClass().getName());
loader=loader.getParent();//将此loader的上级赋给loader
}
System.out.println(loader);
}
}

3、自定义类加载器

(1)自定义的类加载器必须继承抽象类ClassLoader,要覆写其中的findClass(String name)方法,而不用覆写loadClass()方法。
(2)覆写findClass(Stringname)方法的原因:
1)在loadClass()内部是会先委托给父级,当父级找不到后返回,再调用findClass(String name)方法,也就是你自定义的类加载器去找。所以只需要覆写findClass方法,就能实现用自定义的类加载器加载类的目的。
因为,一般自定义类加载器,会把需要加载的类放在自己指定的目录中,而java中已有的类加载器是不知道你这个目录的,所以会找不到。这样才会调用你复写的findClass()方法,用你自定义的类加载器去指定的目录加载类。
2)这是一种模板方法设计模式。这样就保留了loadClass()方法中的流程(这个流程就是先找父级,找不到再调用自定义的类加载器),而我们只需复写findClass方法,实现局部细节就行了。
ClassLoader提供了一个protected Class<?>defineClass(String name, byte[] b, int off, int len)方法,只需要将类对应的class文件传入,就可以将其变为字节码。
(3)编程步骤:
1)编写一个对文件内容进行简单加密的程序
2)编写好了一个自己的类加载器,可实现对加密过来的类进行加载和解密。
3)编写一个程序,调用类加载器加载类,在源程序中不能用该类名定义引用变量,因为编译器无法识别这个类,程序中除了可使用ClassLoader的loadClass方法外,还可以使用设置线程的上下文类加载器或系统类加载器,然后再使用Class.forName。
(4)编码步骤:
1)对不带包名的class文件进行加密,加密结果存放到另外一个目录,例如: java MyClassLoader MyTest.class F:\itcast
2)运行加载类的程序,结果能够被正常加载,但打印出来的类装载器名称为AppClassLoader:java MyClassLoader MyTest F:\itcast
3)用加密后的类文件替换CLASSPATH环境下的类文件,再执行上一步操作就出问题了,错误说明是AppClassLoader类装载器装载失败。
4)删除CLASSPATH环境下的类文件,再执行上一步操作就没问题了。
*/
    示例:
package cn.itheima.demo;  

import java.util.Date;
//定义一个测试类,继承Date,便于使用时加载
public class ClassLoaderAttachment extends Date{
//复写toString方法
public String toString(){
return "Hello World!";
}
}

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) {
// TODO: handle exception
e.printStackTrace();
}
return null;
//return super.findClass(name);
}

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类的实例对象
System.out.println(d1);
}
}

4、类加载器的高级问题分析

 /**
(1)编写一个能打印出自己的类加载器名称和当前类加载器的父子结构关系链的MyServlet,正常发布后,看到打印结果为WebAppClassloader。
(2)把MyServlet.class文件打jar包,放到ext目录中,重启tomcat,发现找不到HttpServlet的错误。
(3)把servlet.jar也放到ext目录中,问题解决了,打印的结果是ExtclassLoader 。
*/
    图例说明:


黑马程序员—【Java高新技术】之内省、注解、类加载器    

    注释:

/**<pre name="code" class="java">开始MyServlet是由WebAppClassoader加载的,但MyServlet又用到了HttpServlet,所以HttpServlet也从WebAppClassoader加载。之后将目录改变后MyServlet变成由ExtclassLoader加载,这时HttpServlet也会跟着变,然后报错,因为只能WebAppClassoader加载HttpServlet。
*/
 


------- android培训java培训、期待与您交流! ---------