本节内容:
- 类加载器
- 反射
- 注解 @xxx
- 动态代理
一、类加载器
1. 什么是类加载器,作用是什么?
类加载器(ClassLoader)就是加载字节码文件(.class)。站在累加载器的角度上看CodeSegment,就是一个一个Class对象。
站在ClassLoader角度来讲,每个class就是一个class对象。而在每个class类对象里,又有属性、方法。
2. ClassLoader的类加载机制
并非一次性加载,需要的时候加载(运行期间动态加载)。不是从头到尾扫描,有多少类就加载进多少类,而是用到的时候才会去加载。
【示例】:TestDynamicLoading.java
package com.itheima.reflection; public class TestDynamicLoading { public static void main(String[] args) { new A(); System.out.println("========="); new B(); new C(); new C(); new D(); new D(); } } class A { } class B { } class C { static { System.out.println("cccccccc"); } } class D { { System.out.println("dddddd"); } }
如果A.class和B.class同时加载进来,那么“======”会在它们加载完成后打印出来;
如果是看到A加载A,看到B才加载B,那么“======”会在它们中间。
下面设置下运行配置,鼠标右击选择“Run As”,“Run Configuration”,(x)= Arguments,在“VM arguments”里输入:
-verbose:class
所以这叫做动态加载。
另外,注意看类C中的static语句块,static语句块中的内容会在类加载进内存后被调用一次,注意无论你生成多少个C的对象,只会调用一次。
而类D中的代码叫动态语句块,每次new出来一个对象的时候就会去执行动态语句块中的内容。动态语句块用的不多。
3. 类加载器的种类
类加载器有三种,不同类加载器加载不同的字节码:
- BootStrap:引导类加载器:加载都是最基础的文件。这是最核心的类加载器。
- 这里的BootStrap和前端学习的BootStrap不是一个东西,前端学习的BootStrap是一个前端框架。这里的BootStrap底层是C语言写的,Java是个高级语言,它是操作不了硬件的。它主要加载的是JVM运行时的最基础的一些jar包。其他的类加载器都是Java写的。其他的类加载器要想工作的话,类加载器本身也是要加载到内存,下面的类加载器都是被BootStrap类加载器加载到内存中,然后其他的类加载器在加载其他的需要的class进内存。
- ExtClassLoader:扩展类加载器:加载都是基础的文件,加载JVM的一些基础性jar包中的类。
- AppClassLoader:应用类加载器:第三方jar包和自己编写java文件。
- 第三方jar包和我们自己编写的jar文件是没区别的。我们自己写的java文件编译后也可以打成一个jar包。
【注意】:上面的关系不是继承,是加载器对象中有一个引用指向了上一层的加载器对象。
怎么获得类加载器?(重点)
ClassLoader 字节码对象.getClassLoader();
获得类加载器之后,可以获得classes下任何资源:
package com.itheima.classloader; public class Demo { public static void main(String[] args) { //获得Demo字节码文件的类加载器 Class clazz = Demo.class;//获得Demo的字节码对象 ClassLoader classLoader = clazz.getClassLoader();//获得类加载器 //getResource()中的的参数路径是相对于classes(src) //获得classes(src)下的任何的资源,这个jdbc.properties被放在了和Demo.java在一起 String path = classLoader.getResource("com/itheima/classloader/jdbc.properties").getPath(); //classLoader.getResourceAsStream(""); System.out.println(path); } }
二、反射
上面的字节码对象(Class对象)怎么获取?3种方式,上面的示例中只是这3种中第一种方式。
方式一,使用类的class属性: Class<java.util.Date> clz1 = java.util.Date.class; 方式二,通过Class类中的静态方法forName(String className),传入类的全限定名(必须添加完整包名)。 Class<?> clz2 = Class.forName(“java.util.Date”); //把名字为java.util.Date的类加载进内存 方式三,通过对象的getClass方法来实现,其中,getClass()是Object类中的方法,所有的对象都可以调用该方法 java.util.Date str = new java.util.Date(); Class<?> clz3 = str.getClass();
反射是在运行期间动态地加载一个类进来,动态地new一个对象出来,动态地去了解这个对象的内部结构,动态地去调用这个对象的某一些方法。在学习框架时,很多的类的名字是写在配置文件中的。这时候你就会想它们是怎么new出来的,就是用反射机制new出来的。
【示例】:建立一个properties文件,把需要动态加载的类名写进去
test.properties
com.itheima.reflection.T
接着写一个程序从properties中把这个类名读出来,然后生成它的一个对象。
TestFlection.java
package com.itheima.reflection; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class TestFlection { public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { String str = "com.itheima.reflection.T"; //必须是类全名,这块从properties文件中读取出来 Class c = Class.forName(str); Object o = c.newInstance(); Method[] methods = c.getMethods(); //获取类里面有哪些方法,这些方法有哪些信息,放到method对象里 for (Method m : methods) { //System.out.println(m.getName()); if(m.getName().equals("mm")) { m.invoke(o); //m.invoke(obj, args) obj:方法属于哪个对象的,args:方法的参数。参数可以有多个,也可以不传,属于可变参数的方法 } if(m.getName().equals("m1")) { for (Class paramType : m.getParameterTypes()) { System.out.println(paramType.getName()); } m.invoke(o, 1, 2); } if(m.getName().equals("getS")) { Class returnType = m.getReturnType(); System.out.println(returnType); System.out.println(returnType.getName()); } } } } class T { int i; String s; static { //可以写个static代码块看看是否被加载成功 System.out.print("T loaded!"); } public T() { System.out.println("T constructed!"); } public void m1(int i, int j) { this.i = i + j; System.out.println(this.i); } public void mm() { System.out.println("m invoked"); } public String getS() { return s; } }
三、注解 @xxx
1. 什么是注解,注解作用
注解就是符合一定格式的语法 @xxxx。比如使用Junit是单元测试的工具,在一个类中使用 @Test 对程序中的方法进行测试。
注解是一种代码级别的说明。它是jdk1.5及以后版本引入的一个特性,与类、接口、枚举是同一个层次。
注解和注释区别:
- 注释:在阅读程序时清楚 --给程序员看的
- 注解:给jvm看的,给机器看的
注解在目前而言最主流的应用:代替配置文件。
- 比如我们在用Eclipse创建动态web工程时,把"Dynamic web module version"选择2.5,这样创建完成后会有web.xml。如果选择3.0的版本,创建完成后会少个web.xml。因为注解可以代替webxml
在src下新建一个Servlet
关于配置文件与注解开发的优缺点:
- 注解优点:开发效率高、成本低
- 注解缺点:耦合性大、并且不利于后期维护
企业中一般是注解和xml混用。基本不改的地方用注解,有可能要改的地方用xml配置文件。
2. jdk5提供的注解
- @Override:JDK5表示告知编译器此方法是覆盖父类的,JDK6还可以表示实现接口的方法。
- @Deprecated:表示被修饰的方法已经过时。过时的方法不建议使用,但仍可以使用。一般被标记为过时的方法都存在不同的缺陷:(1)安全问题;(2)新的API取代。
- @SuppressWarnings:压制警告,被修饰的类或方法如果存在编译警告,将被编译器忽略。
- deprecation:忽略过时
- rawtypes:忽略类型安全
- unused:忽略不使用
- unchecked:忽略安全检查
- null:忽略空指针
- all:忽略所有。如果要忽略多个,可以直接all
import java.util.ArrayList; import java.util.List; /** * 测试JDK5提供的注解。 * @author jkzhao * */ public class AnnoDemo { public static void main(String[] args) { //压制警告 @SuppressWarnings({ "unused", "rawtypes" }) //如果里面要压制的太多,直接"all" List list = new ArrayList(); //这句话会报警告,因为没加泛型,集合中什么都可以存。取的时候自己不知道里面存的什么,可能存在转换错误的隐患。rawtypes //list未使用,也会出警告。unused show(); } //定义方法过时 @Deprecated public static void show(){ } public static void show(String xx){ } //帮助开发人间检查是否覆盖父类的方法正确。比如你把下面的方法改为public String toStringXX(),加上这个注解,就会报错了。帮你做了检查,你写错了。 //注解是给编译器用的,当你写错代码导致错误时,Eclipse会报红线,编译出错了。javac是编译,现在使用IDE工具,不需要自己手动编译,点击菜单栏的"Project",可以看到"Build Automatically" @Override public String toString() { return super.toString(); } }
发现的问题:
不同的注解只能在不同的位置使用(可能在方法上、可能在字段上、可能在类上)
3. 自定义注解(了解)
- 怎样去编写一个自定义的注解
- 怎样去使用注解
- 怎样去解析注解 --使用反射知识解析注解。注解的功能是靠解析去做的
(1)编写一个注解
关键字:@interface
如何定义注解的属性:
语法:返回值 属性名称();
注意:如果属性的名字是value,并且注解的属性值只有一个,那么在使用注解时可以省略value这个key
注解属性类型只能是以下几种
- 基本类型
- String
- 枚举类型
- 注解类型
- Class类型
- 以上类型的一维数组类型
New—>Annotation,输入注解名称,点击完成。
package com.itheima.annotation; public @interface MyAnno { //定义注解的属性,在这里不叫方法 String name(); int age() default 28; //注意:如果属性的名字是value,并且注解的属性值只有一个,那么在使用注解时可以省略value这个key //String value(); //String[] value(); }
(2)使用注解
在类/方法/字段 上面是@XXX
package com.itheima.annotation; @MyAnno(name = "zhangsan") public class MyAnnoTest { @MyAnno(name = "zhangsan") public void show(String str){ System.out.println("show running..."); } }
至于注解的功能是解析的时候实现。
(3)解析使用注解的类
以后使用框架时,这些解析都是框架的作者帮我们写好了,我们只需要会使用即可。
介入一个概念:元注解:代表修饰注解的注解,作用:限制定义的注解的特性。
- @Retention
- SOURCE: 注解在源码级别可见
- CLASS:注解在源码级别和字节码文件级别可见
- RUNTIME:注解在整个运行阶段都可见
- @Target:代表注解修饰的范围:类上使用,方法上使用,字段上使用
- FIELD: 字段上可用此注解
- METHOD: 方法上可以用此注解
- TYPE: 类/接口上可以使用此注解
【注意】:要想解析使用了注解的类 , 那么该注解的Retention必须设置成Runtime。因为是通过反射获得的,所以必须把注解带到运行时。
关于注解解析的实质:从注解中解析出属性值。
字节码对象存在于获得注解相关的方法:
isAnnotationPresent(Class<? extends Annotation> annotationClass) : 判断该字节码对象身上是否使用该注解了 getAnnotation(Class<A> annotationClass) :获得该字节码对象身上的注解对象
【示例】:解析注解
package com.itheima.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnno { //定义注解的属性,在这里不叫方法 String name(); int age() default 28; //注意:如果属性的名字是value,并且注解的属性值只有一个,那么在使用注解时可以省略value这个key //String value(); //String[] value(); }
package com.itheima.annotation; @MyAnno(name = "zhangsan") public class MyAnnoTest { @SuppressWarnings("all") @MyAnno(name = "zhangsan") //@MyAnno({ "aaa","bbb","ccc"}) public void show(String str){ System.out.println("show running..."); } }
package com.itheima.annotation; import java.lang.reflect.Method; public class MyAnnoParser { public static void main(String[] args) throws NoSuchMethodException, SecurityException { //解析show方法上面的@MyAnno //直接的目的是 获得show方法上的@MyAnno中的参数 //获得show方法的字节码对象 Class clazz = MyAnnoTest.class; Method method = clazz.getMethod("show", String.class); //获得show方法上的@MyAnno MyAnno annotation = method.getAnnotation(MyAnno.class); //获得@MyAnno上的属性值 System.out.println(annotation.name());//zhangsan System.out.println(annotation.age());//28 //根据业务需求写逻辑代码 } }
四、动态代理
1. 什么是代理(中介、微商、经纪人)
- 目标对象/被代理对象 -- 房主:拥有真正的租房的方法
- 代理对象 --黑中介:有租房子的方法(其实是调用房主的租房的方法)
- 执行代理对象方法的对象 --租房的人
流程:我们要租房(找不到房主,只能找中介租房)-->中介(租房的方法)-->房主(租房的方法)
抽象:调用对象-->代理对象-->目标对象
2. 动态代理
动态代理:不用手动编写一个代理对象,不需要一一编写与目标对象相同的方法,这个过程,在运行时的内存中动态生成代理对象。 --字节码对象级别的代理对象
动态代理的API:在jdk的API中存在一个Proxy中存在一个生成动态代理的的方法newProxyInstance
返回值:Object就是代理对象
参数:
- loader:代表与目标对象相同的类加载器 --目标对象.getClass().getClassLoader()
- interfaces:代表与目标对象实现的所有的接口字节码对象数组
- h:具体的代理的操作,InvocationHandler接口
【注意】:JDK的Proxy方式实现的动态代理,目标对象必须有接口,没有接口不能实现jdk版动态代理。
【示例1】:动态代理示例。
package com.itheima.dynamicProxy; public interface TargetInterface { public void method1(); public String method2(); public int method3(int x); }
package com.itheima.dynamicProxy; public class Target implements TargetInterface{ @Override public void method1() { System.out.println("method1 running..."); } @Override public String method2() { System.out.println("method2 running..."); return "method2"; } @Override public int method3(int x) { return x; } }
package com.itheima.dynamicProxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import org.junit.Test; public class ProxyTest { @Test public void test1(){ //获得动态的代理对象----在运行时 在内存中动态的为Target创建一个虚拟的代理对象 //objProxy是代理对象 根据参数确定到底是谁的代理对象 TargetInterface objProxy = (TargetInterface) Proxy.newProxyInstance( Target.class.getClassLoader(), //与目标对象相同的类加载器,因为要用这个类加载器去加载那个虚拟的代理对象。(Target.class)有3种方式 new Class[]{TargetInterface.class}, //接口的字节码对象数组 new InvocationHandler() { //InvocationHandler是一个接口,里面只有一个方法。这里需要new这个接口或者这个接口的实现类。匿名内部类 //invoke 代表的是执行代理对象的方法,通过代理对象去调被代理对象的什么方法,都是执行invoke方法 @Override //proxy:是代理对象,就是上面的TargetInterface proxy,不需要管,也不要在这个方法内部用。java中说对象就是维护着这个对象的引用 //method:代表目标对象(被代理对象)的方法字节码对象 //args:代表目标对象的相应的方法的参数 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("目标方法前的逻辑。。。"); //执行目标对象(被代理对象)的方法,通过反射运行该方法 Object invoke = method.invoke(new Target(), args); System.out.println("目标方法后的逻辑。。。"); return invoke; } } ); objProxy.method1(); String method2 = objProxy.method2(); System.out.println(method2); } }
package com.itheima.dynamicProxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class ProxyTest2 { public static void main(String[] args) { final Target target = new Target(); //动态创建代理对象 TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), //通过字节码对象获取这个字节码对象实现的所有接口 new InvocationHandler() { //匿名内部类 @Override //被执行几次?------- 看代理对象调用方法几次 //代理对象调用接口的相应方法 都是调用invoke方法 /* * proxy:是代理对象,就是上面的TargetInterface proxy,不需要管,也不要在这个方法内部用。java中说对象就是维护着这个对象的引用 * method:代表的是目标方法的字节码对象 * args:代表是调用目标方法时参数 */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //反射知识点 Object invoke = method.invoke(target, args);//目标对象的相应方法 //return返回的值给代理对象 return invoke; } } ); proxy.method1();//调用invoke---Method:目标对象的method1方法 args:null 返回值null String method2 = proxy.method2();//调用invoke---Method:目标对象的method2方法 args:null 返回值method2 int method3 = proxy.method3(100);////调用invoke-----Method:目标对象的method3方法 args:Object[]{100} 返回值100 System.out.println(method2); System.out.println(method3); } }
【示例2】:使用动态代理完成全局编码。(与装饰者模式代码放在一起,注意比较)
public class EncodingFilter implements Filter{ @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { //使用动态代理完成全局编码 final HttpServletRequest req = (HttpServletRequest)request; //目标对象(被代理对象) HttpServletRequest enhanceRequest = (HttpServletRequest)Proxy.newProxyInstance( req.getClass().getClassLoader(), req.getClass().getInterfaces(), new InvocationHandler() { //匿名内部类 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //对getParameter方法进行增强 String name = method.getName(); //获得目标对象的方法名称 if("getParameter".equals(name)) { String invoke = (String)method.invoke(req, args); //乱码 //转码后的字符串 invoke = new String(invoke.getBytes("iso8859-1"),"UTF-8"); return invoke; } //其它方法原封不动地执行 return method.invoke(req, args); } } ); chain.doFilter(enhanceRequest, response); //request.setCharacterEncoding("UTF-8"); //在传递request之前对request的getParameter方法进行增强 /* * 装饰者模式(包装) * * 1、增强类与被增强的类要实现统一接口 * 2、在增强类中传入被增强的类 * 3、需要增强的方法重写,不需要增强的方法调用被增强对象的 * */ //被增强的对象 //HttpServletRequest req = (HttpServletRequest) request; //增强对象 //EnhanceRequest enhanceRequest = new EnhanceRequest(req); //chain.doFilter(enhanceRequest, response); } @Override public void destroy() { } @Override public void init(FilterConfig filterConfig) throws ServletException { } } class EnhanceRequest extends HttpServletRequestWrapper{ //HttpServletRequest也是实现了HttpServletRequestWrapper private HttpServletRequest request; public EnhanceRequest(HttpServletRequest request) { super(request); this.request = request; } //对getParameter增强,注意并没有对getParameterMap做增强 @Override public String getParameter(String name) { String parameter = request.getParameter(name);//乱码 try { parameter = new String(parameter.getBytes("iso8859-1"),"UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return parameter; } }
<!-- 编码统一处理的filter --> <filter> <filter-name>EncodingFilter</filter-name> <filter-class>com.ithiema.web.filter.EncodingFilter</filter-class> </filter> <filter-mapping> <filter-name>EncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
实际开发中,装饰者模式一般用于某个对象的方法增强的。动态代理虽然能实现方法增强的效果,但一般不会用在方法增强上,用来拦截,然后做权限。比如判断当前用户是否有权限调用某个方法,这种权限粒度就比较细了。