类加载器、反射、注解和动态代理

本节内容:

  • 类加载器
  • 反射
  • 注解 @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();
    }

}
AnnoDemo.java

发现的问题:

不同的注解只能在不同的位置使用(可能在方法上、可能在字段上、可能在类上)

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();
}
MyAnno.java

(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...");
    }
    
}
MyAnnoTest.java

至于注解的功能是解析的时候实现。

(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();
}
MyAnno.java
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...");
    }
    
}
MyAnnoTest.java
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
        
        //根据业务需求写逻辑代码
        
    }
    
}
MyAnnoParser.java

四、动态代理

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);
}
TargetInterface.java
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;
    }

}
Target.java
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); 
        
    }
}
ProxyTest.java
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);
    }
}
ProxyTest2.java

【示例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;
    }
    
}
EncodingFilter.java
<!-- 编码统一处理的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>
web.xml部分配置

实际开发中,装饰者模式一般用于某个对象的方法增强的。动态代理虽然能实现方法增强的效果,但一般不会用在方法增强上,用来拦截,然后做权限。比如判断当前用户是否有权限调用某个方法,这种权限粒度就比较细了。

原文地址:https://www.cnblogs.com/zhaojiankai/p/7905846.html