`

学习Spring必学的Java基础知识(2)----动态代理

阅读更多
引述要学习Spring框架的技术内幕,必须事先掌握一些基本的Java知识,正所谓“登高必自卑,涉远必自迩”。以下几项Java知识和Spring框架息息相关,不可不学(我将通过一个系列分别介绍这些Java基础知识,希望对大家有所帮助。):

[1] Java反射知识-->Spring IoC :http://www.iteye.com/topic/1123081
[2] Java动态代理-->Spring AOP :http://www.iteye.com/topic/1123293
[3] 属性编辑器,即PropertyEditor-->Spring IoC:http://www.iteye.com/topic/1123628
[4] XML基础知识-->Spring配置:http://www.iteye.com/topic/1123630
[5] 注解-->Spring配置:http://www.iteye.com/topic/1123823
[6] 线程本地变更,即ThreadLocal-->Spring事务管理:http://www.iteye.com/topic/1123824
[7] 事务基础知识-->Spring事务管理:http://www.iteye.com/topic/1124043
[8] 国际化信息-->MVC:http://www.iteye.com/topic/1124044
[9] HTTP报文-->MVC:http://www.iteye.com/topic/1124408



Spring AOP使用动态代理技术在运行期织入增强的代码,为了揭示Spring AOP底层的工作机理,有必要对涉及到的Java知识进行学习。Spring AOP使用了两种代理机制:一种是基于JDK的动态代理;另一种是基于CGLib的动态代理。之所以需要两种代理机制,很大程度上是因为JDK本身只提供接口的代理,而不支持类的代理。

带有横切逻辑的实例

我们通过具体化代码实现上一节所介绍例子的性能监视横切逻辑,并通过动态代理技术对此进行改造。在调用每一个目标类方法时启动方法的性能监视,在目标类方法调用完成时记录方法的花费时间。

代码清单6-2  ForumService:包含性能监视横切代码
package com.baobaotao.proxy;
public class ForumServiceImpl implements ForumService {
	public void removeTopic(int topicId) {
    		
         //①-1开始对该方法进行性能监视
		PerformanceMonitor.begin(
                            "com.baobaotao.proxy.ForumServiceImpl. removeTopic");
		System.out.println("模拟删除Topic记录:"+topicId);
		try {
			Thread.currentThread().sleep(20);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}		

         //①-2结束对该方法进行性能监视
		PerformanceMonitor.end();
	}

	public void removeForum(int forumId) {
          //②-1开始对该方法进行性能监视
		PerformanceMonitor.begin(
"com.baobaotao.proxy.ForumServiceImpl. removeForum");
		System.out.println("模拟删除Forum记录:"+forumId);
		try {
			Thread.currentThread().sleep(40);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}		

         //②-2结束对该方法进行性能监视
		PerformanceMonitor.end();
	}
}

代码清单6-2中粗体表示的代码就是具有横切逻辑特征的代码,每个Service类和每个业务方法体的前后都执行相同的代码逻辑:方法调用前启动PerformanceMonitor,方法调用后通知PerformanceMonitor结束性能监视并给记录性能监视结果。

PerformanceMonitor是性能监视的实现类,我们给出一个非常简单的实现版本,其代码如代码清单6-3所示:

代码清单6-3  PerformanceMonitor
package com.baobaotao.proxy;
public class PerformanceMonitor {
     //①通过一个ThreadLocal保存调用线程相关的性能监视信息
	private static ThreadLocal<MethodPerformace> performanceRecord =        
								new ThreadLocal<MethodPerformance>();
	
    //②启动对某一目标方法的性能监视
     public static void begin(String method) {
		System.out.println("begin monitor...");
		MethodPerformance mp = new MethodPerformance(method);
		performanceRecord.set(mp);
	}
	public static void end() {
		System.out.println("end monitor...");
		MethodPerformance mp = performanceRecord.get();

         //③打印出方法性能监视的结果信息。
		mp.printPerformance();
	}
}

ThreadLocal是将非线程安全类改造为线程安全类的法宝,在9.2节中我们将详细介绍这个Java基础知识。PerformanceMonitor提供了两个方法:通过调用begin(String method)方法开始对某个目标类方法的监视,method为目标类方法的全限定名;而end()方法结束对目标类方法的监视,并给出性能监视的信息。这两个方法必须配套使用。

用于记录性能监视信息的MethodPerformance类的代码如所示:

代码清单6-4  MethodPerformance
package com.baobaotao.proxy;
public class MethodPerformance {
	private long begin;
	private long end;
	private String serviceMethod;
    public MethodPerformance(String serviceMethod){
       this.serviceMethod = serviceMethod;

       //①记录目标类方法开始执行点的系统时间  
       this.begin = System.currentTimeMillis(); 
 
    }
    public void printPerformance(){
     
        //②获取目标类方法执行完成后的系统时间,并进而计算出目标类方法执行时间
        end = System.currentTimeMillis(); 
        long elapse = end - begin;
 
        //③报告目标类方法的执行时间
        System.out.println(serviceMethod+"花费"+elapse+"毫秒。");  
    }
}

通过下面的代码测试拥有性能监视能力的ForumServiceImpl业务方法:

package com.baobaotao.proxy;

public class TestForumService {
	public static void main(String[] args) {
        ForumService forumService = new ForumServiceImpl();
        forumService .removeForum(10);
	   forumService .removeTopic(1012);
	}
}

我们得到以下输出信息:
引用
begin monitor... ①removeForum(10)方法的性能监视报告
模拟删除Forum记录:10
end monitor...
com.baobaotao.proxy.ForumServiceImpl.removeForum花费47毫秒。

begin monitor... ①removeTopic(1012)方法的性能监视报告
模拟删除Topic记录:1012
end monitor...
com.baobaotao.proxy.ForumServiceImpl.removeTopic花费26毫秒。

正如代码清单6 2实例所示,当某个方法需要进行性能监视,就必须调整方法代码,在方法体前后分别添加上开启性能监视和结束性能监视的代码。这些非业务逻辑的性能监视代码破坏了ForumServiceImpl业务逻辑的纯粹性。我们希望通过代理的方式,将业务类方法中开启和结束性能监视的这些横切代码从业务类中完全移除。并通过JDK动态代理技术或CGLib动态代理技术将横切代码动态织入到目标方法的相应位置。

JDK动态代理

JDK 1.3以后,Java提供了动态代理的技术,允许开发者在运行期创建接口的代理实例。在Sun刚推出动态代理时,还很难想象它有多大的实际用途,现在我们终于发现动态代理是实现AOP的绝好底层技术。

JDK的动态代理主要涉及到java.lang.reflect包中的两个类:Proxy和InvocationHandler。其中InvocationHandler是一个接口,可以通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编织在一起。

而Proxy利用InvocationHandler动态创建一个符合某一接口的实例,生成目标类的代理对象。这样讲一定很抽象,我们马上着手使用Proxy和InvocationHandler这两个魔法戒对上一节中的性能监视代码进行革新。

首先,我们从业务类ForumServiceImpl中删除性能监视的横切代码,使ForumServiceImpl只负责具体的业务逻辑,如代码清单6-5所示:

代码清单6-5  ForumServiceImpl:移除性能监视横切代码
package com.baobaotao.proxy;

public class ForumServiceImpl implements ForumService {

	public void removeTopic(int topicId) {
                                ①
		System.out.println("模拟删除Topic记录:"+topicId);
		try {
			Thread.currentThread().sleep(20);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
 		                      ①
	}
	public void removeForum(int forumId) {
       	                  ②
		System.out.println("模拟删除Forum记录:"+forumId);
		try {
			Thread.currentThread().sleep(40);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
       	                  ②
	}
}

在代码清单6-5中的①和②处,原来的性能监视代码被移除了,我们只保留了真正的业务逻辑。

从业务类中移除的性能监视横切代码当然不能漂浮在空气中,它还得找到一个安身之所,InvocationHandler就是横切代码的安家乐园,我们将性能监视的代码安置在PerformanceHandler中,如代码清单6-6所示:

代码清单6-6  PerformanceHandler
package com.baobaotao.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class PerformanceHandler implements InvocationHandler {//①实现InvocationHandler
    private Object target;
	public PerformanceHandler(Object target){ //②target为目标的业务类
		this.target = target;
	}
	public Object invoke(Object proxy, Method method, Object[] args) ③
			throws Throwable {
		PerformanceMonitor.begin(target.getClass().getName()+"."+ method. getName());③-1
		Object obj = method.invoke(target, args);// ③-2通过反射方法调用业务类的目标方法
		PerformanceMonitor.end();③-1
		return obj;
	}
}


③处invoke()方法中粗体所示部分的代码为性能监视的横切代码,我们发现,横切代码只出现一次,而不是原来那样星洒各处。③-2处的method.invoke()语句通过Java反射机制间接调用目标对象的方法,这样InvocationHandler的invoke()方法就将横切逻辑代码(③-1)和业务类方法的业务逻辑代码(③-2)编织到一起了,所以我们可以将InvocationHandler看成是一个编织器。下面,我们对这段代码做进一步的说明。

首先,我们实现InvocationHandler接口,该接口定义了一个 invoke(Object proxy, Method method, Object[] args)的方法,proxy是最终生成的代理实例,一般不会用到;method是被代理目标实例的某个具体方法,通过它可以发起目标实例方法的反射调用;args是通过被代理实例某一个方法的入参,在方法反射调用时使用。

此外,我们在构造函数里通过target传入希望被代理的目标对象,如②处所示,在InvocationHandler接口方法invoke(Object proxy, Method method, Object[] args)里,将目标实例传给method.invoke()方法,调用目标实例的方法,如③所示。
下面,我们通过Proxy结合PerformanceHandler创建ForumService接口的代理实例,如代码清单6-7所示:

代码清单6-7  TestForumService:创建代理实例
package com.baobaotao.proxy;
import java.lang.reflect.Proxy;
public class TestForumService {
	public static void main(String[] args) {
               
               //①希望被代理的目标业务类
		ForumService target = new ForumServiceImpl(); 
		
               //②将目标业务类和横切代码编织到一起
		PerformanceHandler handler = new PerformanceHandler(target);
		
                //③根据编织了目标业务类逻辑和性能监视横切逻辑的InvocationHandler实例创建代理实例
		ForumService proxy = (ForumService) Proxy.newProxyInstance(  
				target.getClass().getClassLoader(),
				target.getClass().getInterfaces(),
				handler);

                //④调用代理实例
		proxy.removeForum(10);   
		proxy.removeTopic(1012);
	}
}

上面的代码完成业务类代码和横切代码的编织工作并生成了代理实例。在②处,我们让PerformanceHandler将性能监视横切逻辑编织到ForumService实例中,然后在③处,通过Proxy的newProxyInstance()静态方法为编织了业务类逻辑和性能监视逻辑的handler创建一个符合ForumService接口的代理实例。该方法的第一个入参为类加载器;第二个入参为创建代理实例所需要实现的一组接口;第三个参数是整合了业务逻辑和横切逻辑的编织器对象。

按照③处的设置方式,这个代理实例实现了目标业务类的所有接口,即Forum ServiceImpl的ForumService接口。这样,我们就可以按照调用ForumService接口实例相同的方式调用代理实例,如④所示。运行以上的代码,输出以下信息:
引用
begin monitor...
模拟删除Forum记录:10
end monitor...
com.baobaotao.proxy.ForumServiceImpl.removeForum花费47毫秒。

begin monitor...
模拟删除Topic记录:1012
end monitor...
com.baobaotao.proxy.ForumServiceImpl.removeTopic花费26毫秒。

我们发现,程序的运行效果和直接在业务类中编写性能监视逻辑的效果一致,但是在这里,原来分散的横切逻辑代码已经被我们抽取到PerformanceHandler中。当其他业务类(如UserService、SystemService等)的业务方法也需要使用性能监视时,我们只要按照代码清单6-7相似的方式,分别为它们创建代理对象就可以了。下面,我们通过时序图描述通过创建代理对象进行业务方法调用的整体逻辑,以进一步认识代理对象的本质,如图6-3所示。





我们在上图中使用虚线的方式对通过Proxy创建的ForumService代理实例加以凸显,ForumService代理实例内部利用PerformaceHandler整合横切逻辑和业务逻辑。调用者调用代理对象的removeForum()和removeTopic()方法时,上图的内部调用时序清晰地告诉我们实际上所发生的一切。

CGLib动态代理

使用JDK创建代理有一个限制,即它只能为接口创建代理实例,这一点我们可从Proxy的接口newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)的方法签名中就看得很清楚:第二个入参interfaces就是需要代理实例实现的接口列表。虽然面向接口编程的思想被很多大师级人物(包括Rod Johnson)推崇,但在实际开发中,许多开发者也对此深感困惑:难道对一个简单业务表的操作也需要老老实实地创建5个类(领域对象类、Dao接口,Dao实现类,Service接口和Service实现类)吗?难道不能直接通过实现类构建程序吗?对于这个问题,我们很难给出一个孰好孰劣的准确判断,但我们确实发现有很多不使用接口的项目也取得了非常好的效果(包括大家所熟悉的SpringSide开源项目)。

对于没有通过接口定义业务方法的类,如何动态创建代理实例呢?JDK的代理技术显然已经黔驴技穷,CGLib作为一个替代者,填补了这个空缺。

CGLib采用非常底层的字节码技术,可以为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,并顺势织入横切逻辑。下面,我们采用CGLib技术,编写一个可以为任何类创建织入性能监视横切逻辑代理对象的代理创建器,如代码清单 6-8所示:

代码清单6-8  CglibProxy
package com.baobaotao.proxy;
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class CglibProxy implements MethodInterceptor {
	private Enhancer enhancer = new Enhancer();
	public Object getProxy(Class clazz) {
		enhancer.setSuperclass(clazz); //① 设置需要创建子类的类
		enhancer.setCallback(this); 
		return enhancer.create(); //②通过字节码技术动态创建子类实例
 
	}

        //③拦截父类所有方法的调用
	public Object intercept(Object obj, Method method, Object[] args, 
			MethodProxy proxy) throws Throwable {
		PerformanceMonitor.begin(obj.getClass().getName()+"."+method. getName());//③-1
		Object result=proxy.invokeSuper(obj, args); ③-2 
		PerformanceMonitor.end();//③-1通过代理类调用父类中的方法
		return result;
	}
}

在上面代码中,用户可以通过getProxy(Class clazz)为一个类创建动态代理对象,该代理对象通过扩展clazz创建代理对象。在这个代理对象中,我们织入性能监视的横切逻辑(③-1)。intercept(Object obj, Method method, Object[] args,MethodProxy proxy)是CGLib定义的Interceptor接口的方法,它拦截所有目标类方法的调用,obj表示目标类的实例;method为目标类方法的反射对象;args为方法的动态入参;而proxy为代理类实例。

下面,我们通过CglibProxy为ForumServiceImpl类创建代理对象,并测试代理对象的方法,如代码清单6-9所示:

代码清单6-9  TestForumService:测试Cglib创建的代理类
package com.baobaotao.proxy;
import java.lang.reflect.Proxy;
public class TestForumService {
	public static void main(String[] args) {
	  CglibProxy proxy = new CglibProxy();
	  ForumServiceImpl forumService = ① 
	            (ForumServiceImpl )proxy.getProxy(ForumServiceImpl.class);
	  forumService.removeForum(10);
	  forumService.removeTopic(1023);
	}
}


在①中,我们通过CglibProxy为ForumServiceImpl动态创建了一个织入性能监视逻辑的代理对象,并调用代理类的业务方法。运行上面的代码,输入以下信息:
引用
begin monitor...
模拟删除Forum记录:10
end monitor...
com.baobaotao.proxy.ForumServiceImpl$$EnhancerByCGLIB$$2a9199c0.removeForum花费47毫秒。
begin monitor...
模拟删除Topic记录:1023
end monitor...
com.baobaotao.proxy.ForumServiceImpl$$EnhancerByCGLIB$$2a9199c0.removeTopic花费16毫秒。

观察以上的输出,除了发现两个业务方法中都织入了性能监控的逻辑外,我们还发现代理类的名字是com.baobaotao.proxy.ForumServiceImpl$$EnhancerByCGLIB$$2a9199c0,这个特殊的类就是CGLib为ForumServiceImpl动态创建的子类。

代理知识小结

Spring AOP的底层就是通过使用JDK动态代理或CGLib动态代理技术为目标Bean织入横切逻辑。在这里,我们对前面两节动态创建代理对象作一个小结。

我们虽然通过PerformanceHandler或CglibProxy实现了性能监视横切逻辑的动态织入,但这种实现方式存在三个明显需要改进的地方:

1)目标类的所有方法都添加了性能监视横切逻辑,而有时,这并不是我们所期望的,我们可能只希望对业务类中的某些特定方法添加横切逻辑;
2)我们通过硬编码的方式指定了织入横切逻辑的织入点,即在目标类业务方法的开始和结束前织入代码;
3)我们手工编写代理实例的创建过程,为不同类创建代理时,需要分别编写相应的创建代码,无法做到通用。

以上三个问题,在AOP中占用重要的地位,因为Spring AOP的主要工作就是围绕以上三点展开:Spring AOP通过Pointcut(切点)指定在哪些类的哪些方法上织入横切逻辑,通过Advice(增强)描述横切逻辑和方法的具体织入点(方法前、方法后、方法的两端等)。此外,Spring通过Advisor(切面)将Pointcut和Advice两者组装起来。有了Advisor的信息,Spring就可以利用JDK或CGLib的动态代理技术采用统一的方式为目标Bean创建织入切面的代理对象了。

JDK动态代理所创建的代理对象,在JDK 1.3下,性能强差人意。虽然在高版本的JDK中,动态代理对象的性能得到了很大的提高,但是有研究表明,CGLib所创建的动态代理对象的性能依旧比JDK的所创建的代理对象的性能高不少(大概10倍)。但CGLib在创建代理对象时所花费的时间却比JDK动态代理多(大概8倍),所以对于singleton的代理对象或者具有实例池的代理,因为无须频繁创建代理对象,所以比较适合用CGLib动态代理技术,反之适合用JDK动态代理技术。值得一提的是,由于CGLib采用动态创建子类的方式生成代理对象,所以不能对目标类中的final方法进行代理。

这些文章摘自于我的《Spring 4.x企业应用开发实战》,我将通过连载的方式,陆续在此发出。欢迎大家讨论。
  • 大小: 73.7 KB
分享到:
评论
3 楼 iSmile 2013-04-28  
确实不错!
2 楼 yixi2166 2012-06-27  
      
1 楼 飞天奔月 2012-05-23  
不得不说 陈老师对spring的理解 已经出神入化的地步了

通过陈老师的书 学到不少东西

相关推荐

    主题:学习Spring必学的Java基础知识(1)----反射

    主题:学习Spring必学的Java基础知识(1)----反射主题:学习Spring必学的Java基础知识(1)----反射主题:学习Spring必学的Java基础知识(1)----反射

    学习Spring必学的Java基础知识(含数据库事务基础知识)

    学习Spring必学的Java基础知识(含数据库事务基础知识)

    学习Spring必学的Java基础知识(3)—PropertyEditor

    在深入学习Spring框架之前,掌握基础的Java知识是至关重要的,特别是关于`PropertyEditor`的部分。`PropertyEditor`是Java中用于类型转换的关键组件,它允许我们自定义数据类型的转换规则,这对于处理用户输入或者在...

    学习java必看的书籍

    ### 学习Java必看的书籍 在Java学习过程中,选择合适的书籍对于深入理解这门语言至关重要。根据提供的部分信息,我们将重点介绍三本被广泛推荐的经典Java书籍:《Thinking in Java》、《Java Collections》以及...

    Spring基础知识汇总Java开发必看

    学习Spring基础知识对Java开发者至关重要,它能帮助你构建高效、可维护的后端应用,并为后续深入学习Spring全家桶打下坚实基础。在实际项目中,结合其他Spring模块和最佳实践,可以构建出满足现代企业需求的复杂系统...

    java该怎么学-java学习方法-路线图

    本文将基于“java该怎么学-java学习方法-路线图”的主题进行深入探讨,旨在为学习者提供一条清晰且实用的学习路径,帮助大家避免在学习过程中浪费时间在那些目前不重要的技术上。 #### 一、明确学习目标 首先,...

    java学习必学系统--jive

    Java学习之路不可或缺的一环是深入理解并掌握Jive系统。Jive是一款强大的企业社交网络平台,它为企业提供了内部协作、沟通和知识管理的解决方案。在Java开发者的学习路径中,掌握Jive不仅能提升你的Web开发技能,还...

    springcoud的java基础篇学习资料

    在Spring Cloud的学习过程中,Java基础知识是必不可少的一部分。Java作为Spring Cloud的基础语言,其核心概念和技术对理解Spring Cloud的实现机制至关重要。以下将详细介绍Java在Spring Cloud中的应用以及相关的知识...

    Java Spring学习路线.pdf

    AOP是通过预编译和运行时动态代理实现的,可以将横切关注点与业务逻辑分离。Spring的AOP支持通过XML和注解两种方式进行。 - AOP的概述和底层实现原理是理解Spring AOP的基础。 - AspectJ注解开发和AOP通知类型是...

    基于java的-104-202202059. 学习啦!--MHK在线学习小程序-源码.zip

    1. Java编程:作为主要开发语言,理解Java基础语法、面向对象编程和异常处理是必不可少的。 2. Spring Boot/MVC:这是后端服务的基础框架,需要了解其依赖注入、自动配置和MVC设计模式。 3. MyBatis:可能是数据库...

    java基础知识详解 Java入门必看 Java面试题集锦

    这份资料集主要涵盖Java基础知识、初学者入门指南以及常见的Java面试题,旨在帮助学习者系统地理解和掌握Java的核心概念。 首先,Java基础知识是理解任何Java程序的基础。这包括: 1. **Java环境搭建**:学习如何...

    学习java必看

    "学习Java必看"这个压缩包文件显然包含了一系列与Java学习相关的资源,可能是教程文档、代码示例或者教学视频,旨在帮助初学者和有经验的开发者深化对Java的理解。以下是基于这个主题的Java知识点详细说明: 1. **...

    Java学习、面试必备

    1. **Java基础知识** - 类与对象:Java中的所有程序都是由类构建的,对象则是类的实例。理解封装、继承和多态是Java OOP(面向对象编程)的基础。 - 异常处理:Java提供了try-catch-finally结构来处理程序运行时...

    spring-mvc-step-by-step中文版

    ### Spring MVC Step-by-Step中文版知识点概览 #### 一、Spring框架核心概念与组成部分 **1.... - **定义**:控制反转是一种...对于希望深入了解Spring MVC并具备一定Java基础的学习者来说,这份教程将是宝贵的资源。

    spring-framework-4.3.8.RELEASE官方完整包加官方文档

    总而言之,Spring Framework 4.3.8.RELEASE官方完整包提供了完整的开发环境,涵盖了从基础到高级的Spring使用技巧,是Java开发者不可或缺的工具。通过深入学习和实践,开发者可以充分利用Spring的强大功能,提升应用...

    Java最新2024学习路线+Java自学全套免费网盘资料

    这些知识点构成了从Java基础知识到企业级应用开发的完整学习体系,适用于从初学者到有一定经验的开发者逐步进阶。对于想要成为架构师的同学,还需要在系统设计、性能优化、高可用架构等方面进行深入研究和实践。

    java学习计划2023-.zip

    在Java学习过程中,一个系统的学习计划能够帮助初学者...这个学习计划覆盖了从基础到进阶的全面Java知识体系,适合希望系统学习Java的人群。遵循这样的计划,开发者可以逐步提升技能,为从事Java开发工作做好充分准备。

    java基础应用工具-64位.rar

    由于压缩包的具体内容未知,以上只是基于Java基础知识的普遍讲解。实际的学习资料可能包括源代码示例、教程文档、练习题等,帮助用户深入理解和实践Java编程。对于进一步提升,可以学习Spring框架、MyBatis、JPA等...

    java学习知识点,从何学起

    接着,深入学习Java的基础知识。这包括变量、数据类型、控制结构(如if-else、switch、for、while循环)、类与对象、封装、继承、多态等面向对象编程的概念。同时,要掌握异常处理、文件I/O、集合框架(如ArrayList...

Global site tag (gtag.js) - Google Analytics