Java-RMI
[TOC]
概述
Java RMI(Java Remote Method Invocation),即Java远程方法调用,是Java中一种用于实现远程过程调用的应用程序编程接口。允许运行在一个java虚拟机的对象调用运行在另一个java虚拟机上对象的方法。这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。
代理
在客户端为远程对象安装一个代理。代理是位于客户端虚拟机中的一个对象,它对于客户端程序来说,就像是要访问的远程对象一样。客户端调用此代理时,只需进行常规的方法调用。而客户端代理则负责使用网络协议与服务器进行联系。网络模型如下:
存根(stub)
当客户端要调用远程对象的一个方法时,实际上调用的是代理对象上的一个普通方法,我们称此代理对象为存根(stub)。存根位于客户端机器上,而非服务器上。
参数编组
存根会将远程方法所需的参数打包成一组字节,对参数编码的过程就称为参数编组。参数编组的目的是将参数转换成适合在虚拟机之间进行传递的格式,在RMI协议中,对象是使用序列化机制进行编码的。
RMI接口发布和调用流程
- 1、定义一个远程接口,必须继承Remote接口,其中需要远程调用的方法必须抛出RemoteException异常。
在Java中,只要一个类extends了java.rmi.Remote接口,即可成为存在于服务器端的远程对象,供客户端访问并提供一定的服务。JavaDoc描述:Remote 接口用于标识其方法可以从非本地虚拟机上调用的接口。任何远程对象都必须直接或间接实现此接口。只有在“远程接口”(扩展 java.rmi.Remote 的接口)中指定的这些方法才可被远程调用。 - 2、创建远程接口的实现类,继承UnicastRemoteObject类实现序列化,必须显式定义无参构造方法。
远程对象必须实现java.rmi.server.UniCastRemoteObject类,这样才能保证客户端访问获得远程对象时,该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根”,而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的存根是客户端的一个代理,用于与服务器端的通信,而骨架也可认为是服务器端的一个代理,用于接收客户端的请求之后调用远程方法来响应客户端的请求。 - 3、通过LocateRegistry.createRegistry()创建远程对象注册表Registry的实例,被创建的Registry服务将在指定的端口上侦听到来的请求(默认端口是1099)
- 4、通过Naming.bind()将远程服务实现类绑定到指定的RMI地址上,执行这个方法后,相当于发布了RMI服务。
- 5、客户端:通过Naming.lookup()在远程对象注册表Registry中查找指定name的对象,并返回远程对象的引用(一个stub),之后可通过stub调用远程对象的方法。
RMI接口发布调用实例
本实例是一个maven多模块项目,简介如下:
- rmi项目:多模块maven项目的父项目,不含任何代码,只在pom中规定各子模块依赖项的版本号
- rmi-server项目:服务端项目,发布rmi接口
- rmi-client项目:客户端项目,调用rmi接口
服务端项目rmi-server
远程接口HelloService
定义一个远程接口,必须继承Remote接口,其中需要远程调用的方法必须抛出RemoteException异常。
HelloService.java:
package com.masikkk.rmi.server;
import java.rmi.Remote;
import java.rmi.RemoteException;
//远程接口,必须继承Remote接口,其中所有需要远程调用的方法都必须抛出RemoteException异常
public interface HelloService extends Remote{
public String sayHello(String name) throws RemoteException;
public int sum(int a,int b) throws RemoteException;
}
远程接口实现类HelloServiceImpl
HelloServiceImpl.java:
package com.masikkk.rmi.server;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
//远程接口实现类,必须继承java.rmi.server.UniCastRemoteObject类
public class HelloServiceImpl extends UnicastRemoteObject implements HelloService{
private static final long serialVersionUID = 4126819767704786465L;
//如果父类的无参构造函数抛出了异常,则子类的无参构造函数不能省略不写,并且必须抛出父类的异常或父类异常的父类
public HelloServiceImpl() throws RemoteException {
super();
}
@Override
public String sayHello(String name){
return "Hello, " + name;
}
@Override
public int sum(int a, int b){
return a+b;
}
}
远程对象必须实现java.rmi.server.UniCastRemoteObject类,这样才能保证客户端访问获得远程对象时,该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根。
同时,因为UnicastRemoteObject类的默认构造方法抛出了RemoteException异常,所以实现类不能缺省无参构造方法,必须显式定义无参构造方法。
接口发布类HelloServiceMain
创建应用类,注册和启动服务端RMI,以被客户端调用。
HelloServiceMain.java:
package com.masikkk.rmi.server;
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class HelloServiceMain {
public static void main(String[] args) {
try {
HelloService helloService = new HelloServiceImpl();
//创建远程对象注册表Registry的实例,被创建的Registry将在指定的端口(默认1099)上侦听到来的请求
LocateRegistry.createRegistry(8889);
//将远程服务实现类绑定到指定的RMI地址上,执行这个方法后,相当于发布了RMI服务
Naming.bind("rmi://localhost:8889/HelloService", helloService);
System.out.println("远程对象绑定成功!");
} catch (MalformedURLException e) {
System.out.println("发生URL协议异常!");
e.printStackTrace();
} catch (AlreadyBoundException e) {
System.out.println("发生重复绑定对象异常!");
e.printStackTrace();
} catch (RemoteException e) {
System.out.println("创建远程对象发生异常!");
e.printStackTrace();
}
}
}
Run As->Java Application 启动服务端即发布rmi接口。
客户端项目rmi-client
客户端调用类HelloClient
package com.masikkk.rmi.client;
import java.rmi.Naming;
import com.masikkk.rmi.server.HelloService;
public class HelloClient {
public static void main(String[] args) {
try {
//在远程对象注册表Registry中查找指定name的对象,并返回远程对象的引用
HelloService helloService = (HelloService)Naming.lookup("rmi://localhost:8889/HelloService");
System.out.println(helloService.sayHello("masikkk.com"));
System.out.println(helloService.sum(2, 3));
} catch (Exception e) {
e.printStackTrace();
}
}
}
因为客户端需要有服务端那边提供的接口,才可以访问,所以要将服务端的IHello接口完全拷贝(连同包)到客户端,当然为了方便,你在客户端工程中新建一个完全一样的接口也可以。实际运用中通常是要服务端接口打成jar包来提供的。这里我通过配置maven依赖引入服务端项目rmi-server:
<!-- 自己封装的rmi-server项目 -->
<dependency>
<groupId>com.masikkk.rmi</groupId>
<artifactId>rmi-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
参考
对JAVA RMI的认识
http://www.cnblogs.com/dongguacai/p/5617698.htmlRMI原理揭秘之远程对象(不继承UnicastRemoteObject带来的影响,分析很深入)
http://guojuanjun.blog.51cto.com/277646/1423392/java RMI原理详解
http://blog.csdn.net/xinghun_4/article/details/45787549Java RMI实现以及Spring封装RMI实现小结
http://blog.csdn.net/zmx729618/article/details/52130722
RMI与Spring整合
Spring RMI中,主要涉及两个类:org.springframework.remoting.rmi.RmiServiceExporter和org.springframework.remoting.rmi.RmiProxyFactoryBean
服务端使用RmiServiceExporter暴露RMI远程方法,客户端用RmiProxyFactoryBean间接调用远程方法。
RMI与Spring整合实例
本实例是一个maven多模块项目,和上面的原生java RMI实例在同一个父项目中,简介如下:
- rmi项目:多模块maven项目的父项目,不含任何代码,只在pom中规定各子模块依赖项的版本号
- rmi-spring-server项目:spring服务端项目,发布rmi接口
- rmi-spring-client项目:spring客户端项目,调用rmi接口
服务端项目rmi-spring-server
maven依赖
<dependencies>
<!-- JUnit4 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
接口HelloSpringService
HelloSpringService.java:
package com.masikkk.rmi.spring.server;
//普通接口
public interface HelloSpringService{
public String sayHello(String name);
public int sum(int a,int b);
}
服务端创建普通接口,无需继承其他接口。
接口实现类HelloSpringServiceImpl
HelloSpringServiceImpl.java:
package com.masikkk.rmi.spring.server;
//接口实现类
public class HelloSpringServiceImpl implements HelloSpringService{
@Override
public String sayHello(String name){
return "Spring:Hello, " + name;
}
@Override
public int sum(int a, int b){
return a+b;
}
}
服务端Spring上下文配置文件
编辑Java Build path,创建src/main/resources文件夹,在其中新建服务端Spring bean配置文件applicationContext-server.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- Service实现类 -->
<bean id="helloSpringServiceImpl" class="com.masikkk.rmi.spring.server.HelloSpringServiceImpl">
</bean>
<!-- 将服务类导出为RMI服务 -->
<bean id="springRMIServer" class="org.springframework.remoting.rmi.RmiServiceExporter">
<property name="serviceName" value="rmiHelloSpringService"></property>
<property name="service" ref="helloSpringServiceImpl"></property>
<property name="serviceInterface" value="com.masikkk.rmi.spring.server.HelloSpringService"></property>
<property name="registryPort" value="8899"></property>
</bean>
</beans>
用RmiServiceExporter暴露RMI接口时,需要配置的property有:
- serviceName,发布的服务名称
- service,接口实现类
- serviceInterface,接口
- registryPort,端口
最终的服务地址为:rmi://localhost:{registryPort}/{serviceName}
服务端启动类HelloSpringServiceMain
package com.masikkk.rmi.spring.server;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class HelloSpringServiceMain {
public static void main(String[] args) {
new ClassPathXmlApplicationContext("applicationContext-server.xml");
System.out.println("Spring远程对象绑定成功!");
}
}
客户端项目rmi-spring-client
maven依赖
<dependencies>
<!-- 自己封装的rmi-spring-server -->
<dependency>
<groupId>com.masikkk.rmi</groupId>
<artifactId>rmi-spring-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- JUnit4 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
客户端Spring上下文配置文件
创建客户端Spring bean配置文件applicationContext-client.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 客户端代理 -->
<bean id="rmiClient" class="org.springframework.remoting.rmi.RmiProxyFactoryBean">
<property name="serviceInterface" value="com.masikkk.rmi.spring.server.HelloSpringService"></property>
<property name="serviceUrl" value="rmi://localhost:8899/rmiHelloSpringService"></property>
</bean>
</beans>
注意属性serviceUrl的值,端口号和serviceName要与RmiServiceExporter中配置一致。
客户端JUnit测试类HelloSpringServiceTest
package com.masikkk.rmi.spring.client;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.masikkk.rmi.spring.server.HelloSpringService;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:applicationContext-client.xml"})
public class HelloSpringServiceTest {
@Autowired
HelloSpringService helloSpringService; //自动装配rmiClient
@Test
public void test(){
System.out.println(helloSpringService.sayHello("masikkk.spring"));
System.out.println(helloSpringService.sum(10, 5));
}
}
参考
- Java RMI实现以及Spring封装RMI实现小结
http://blog.csdn.net/zmx729618/article/details/52130722
GitHub项目源码
本文中所有代码已分享到GitHub,repo地址:https://github.com/masikkk/java-rmi ,是一个多模块maven项目,可导入为maven工程运行。
项目介绍
本项目是一个maven多模块项目,简介如下:
- rmi项目:多模块maven项目的父项目,不含任何代码,只在pom中规定各子模块依赖项的版本号
- rmi-server项目:原生java实现rmi的服务端项目,发布rmi接口
- rmi-client项目:原生java实现rmi的客户端项目,调用rmi接口
- rmi-spring-server项目:rmi与spring整合的服务端项目,发布rmi接口
- rmi-spring-client项目:rmi与spring整合的客户端项目,调用rmi接口
运行方法
- 原生java实现rmi实例:
- 首先运行rmi-server项目发布接口:Run As->Java Application运行RMI接口发布类HelloServiceMain
- 然后运行rmi-client项目调用接口:Run As->Java Application运行RMI接口调用类HelloClient
- rmi与spring整合实例:
- 首先运行rmi-spring-server项目发布接口:Run As->Java Application运行服务端启动类HelloSpringServiceMain
- 然后运行rmi-spring-client项目调用接口:Run As->JUnit Test运行客户端JUnit测试类HelloSpringServiceTest
参考博文
- Java-RMI http://masikkk.com/article/Java-RMI/
相关类和接口
Remote
java.rmi.Remotepublic interface Remote
Remote 接口用于标识其方法可以从非本地虚拟机上调用的接口。任何远程对象都必须直接或间接实现此接口。只有在“远程接口”(扩展 java.rmi.Remote 的接口)中指定的这些方法才可远程使用。
Remote接口是标记接口,无方法。继承Remote接口的远程接口中的方法必须抛出RemoteException异常。
实现类可以实现任意数量的远程接口,并且可以扩展其他远程实现类。RMI 提供一些远程对象实现可以扩展的有用类,这些类便于远程对象创建。这些类是 java.rmi.server.UnicastRemoteObject 和 java.rmi.activation.Activatable。
UnicastRemoteObject
java.rmi.server.UnicastRemoteObjectpublic class UnicastRemoteObject extends RemoteServer
用于导出带 JRMP 的远程对象和获得与该远程对象通信的 stub。
对于下面的构造方法和静态 exportObject 方法,正在导出的远程对象的 stub 按以下方式获得:
- 如果使用 UnicastRemoteObject.exportObject(Remote) 方法导出该远程对象,则加载 stub 类(通常使用 rmic 工具从远程对象的类预生成)并按以下方式构造 stub 类的实例。
- “根类”按以下情形确定:如果远程对象的类直接实现扩展 Remote 的接口,则远程对象的类为根类;否则,根类为直接实现扩展 Remote 接口的远程对象类的最具派生能力的超类。
- 要加载的 stub 类的名称通过连接带有后缀 “_Stub” 的根类的二进制名称确定。
- 按使用根类的类加载器的名称加载 stub 类。该 stub 类必须扩展 RemoteStub 并且必须有公共构造方法,该构造方法有一个属于类型 RemoteRef 的参数。
- 最后,用 RemoteRef 构造 stub 类的实例。
- 如果无法找到适当的 stub 类,或无法加载 stub 类,或创建 stub 实例时出现问题,则抛出 StubNotFoundException。
- 对于所有其他导出方式:
- 如果无法加载远程对象的 stub 类(如上所述)或将系统属性 java.rmi.server.ignoreStubClasses 设置为 “true”(不分大小写),则用以下属性构造 Proxy 实例:
代理的实例由远程对象类的类加载器定义。
该代理实现由远程对象类实现的所有远程接口。
代理的调用处理程序是用 RemoteRef 构造的 RemoteObjectInvocationHandler 实例。
如果无法创建代理,则抛出 StubNotFoundException。 - 否则,将远程对象的 stub 类(如上所述)的实例用作 stub。
LocateRegistry
java.rmi.registry.LocateRegistrypublic final class LocateRegistry extends Object
LocateRegistry 用于获取特定主机(包括本地主机)上的远程对象注册表的引用,或用于创建一个接受对特定端口调用的远程对象注册表。
注意,getRegistry 调用并不实际生成到远程主机的连接。它只创建对远程注册表的本地引用,即便远程主机上没有正运行的注册表,它也会成功创建一个引用。因此,调用作为此方法的结果返回的远程注册表的后续方法可能会失败。
createRegistry()
public static Registry createRegistry(int port)
创建并导出接受指定 port 请求的本地主机上的 Registry 实例。
导出 Registry 实例与调用静态 UnicastRemoteObject.exportObject 方法一样,都是将传入 Registry 实例和指定的 port 作为参数,只不过导出的 Registry 实例具有已知对象的标识标符(用值 ObjID.REGISTRY_ID 构造的 ObjID 实例)。
参数:port - 注册表在其上接受请求的端口
返回:注册表
抛出: RemoteException - 如果无法导出注册表
- java.rmi.Naming和java.rmi.registry.LocateRegistry的区别
http://blog.csdn.net/xinghun_4/article/details/45772175
Naming
java.rmi.Namingpublic final class Naming extends Object
Naming类提供存储和获得“远程对象注册表”上远程对象的引用的方法。Naming 类的每个方法都可将某个名称作为其一个参数,该名称是使用以下形式的 URL 格式(没有 scheme 组件)的 java.lang.String: //host:port/name
。一个//host:port/name
可以唯一定位一个RMI服务器上的发布了的对象。
其中 host 是注册表所在的主机(远程或本地),port 是注册表接受调用的端口号,name 是未经注册表解释的简单字符串。host 和 port 两者都是可选项。如果省略了 host,则主机默认为本地主机。如果省略了 port,则端口默认为 1099,该端口是 RMI 的注册表 rmiregistry 使用的“著名”端口。
为远程对象 绑定 名称是指为远程对象关联或注册一个名称,以后可以使用该名称来查找该远程对象。可以使用 Naming 类的 bind 或 rebind 方法将远程对象与某个名称相关联。
一旦远程对象向本地主机上 RMI 注册表注册(绑定),远程(或本地)主机上的调用方可以通过名称查找远程对象,获得其引用,并在该对象上调用远程方法。注册表可由在一个主机上运行的所有服务器共享,需要时个别服务器进程也可以创建和使用自己的注册表。
实际上,从源码中可以看出,Naming类中的上述方法都是通过Registry类对“远程对象注册表”进行操作的。
注意: Naming类只是在“远程对象注册表”上进行存储和读取操作,该类并不能创建“远程对象注册表”。
bind()
public static void bind(String name, Remote obj)
将指定 name 绑定到远程对象。
参数:
- name - 使用 URL 格式(不含 scheme 组件)的名称
- obj - 远程对象的引用(通常是一个 stub)
抛出:
- AlreadyBoundException - 如果已经绑定了名称
- MalformedURLException - 如果该名称不是适当格式化的 URL
- RemoteException - 如果无法联系注册表
- AccessException - 如果不允许进行此操作(例如,如果起源于非本地主机)
源码:
public static void bind(String name, Remote obj)
throws AlreadyBoundException,
java.net.MalformedURLException,
RemoteException
{
ParsedNamingURL parsed = parseURL(name);
Registry registry = getRegistry(parsed);
if (obj == null)
throw new NullPointerException("cannot bind to null");
registry.bind(parsed.name, obj);
}
lookup()
public static Remote lookup(String name)
返回与指定 name 关联的远程对象的引用(一个 stub)。
参数:name - 使用 URL 格式(不含 scheme 组件)的名称
返回:远程对象的引用
抛出:
- NotBoundException - 如果当前未绑定名称
- RemoteException - 如果无法联系注册表
- AccessException - 如果不允许执行此操作
- MalformedURLException - 如果名称不是适当格式化的 URL
源码:
public static Remote lookup(String name)
throws NotBoundException,
java.net.MalformedURLException,
RemoteException
{
ParsedNamingURL parsed = parseURL(name);
Registry registry = getRegistry(parsed);
if (parsed.name == null)
return registry;
return registry.lookup(parsed.name);
}
RMI codebase
codebase问题其实是一个怎样从网络上下载类的问题,我想不只是在Jini和RMI程序开发中要用到。只要需要从网络上下载类,就要涉及到codebase问题。例如applet等。但是因为我对applet程序不是很熟悉,所以我就只谈Jini和RMI,但我想codebase问题应该是通用的。
毫无疑问,对于大多数Jini和RMI开发新手来说,如何使用codebase是比较轻易令人迷惑的一件事。下面我就讲讲codebase要注重的几个方面,也就是容 易出问题的地方。
一、为什么需要codebase
当我们用一个对象作为远程方法调用的参数时,对象是以序列化流来传输到远端,然后在远端重新生成对象。这样就可能在两个Java虚拟机中交换对象了。但是序列化是这种传递对象的一部分。当你序列化对象时,你仅仅是把对象的成员数据转化成字节流,而实际实现该对象的代码却没有。也就是说,传递的只是数据部分,而做为控制逻辑的程序代码部分却没有被传递。这就是RMI初学者轻易误解的地方,我已经序列化对象了,而且对象也传过去了,怎么还说找不到呢。其实,对象数据的确过去了,不过找不到是类定义,这个并不是序列化传过去的,RMI协议是不传递代码的。但是,对于本地没有的类文件的对象,RMI提供了一些机制答应接收对象的一方去取回该对象的类代码。而到什么地方去取,这就需要发送方设置codebase了。
二、什么是codebase
简单说,codebase就是远程装载类的路径。当对象发送者序列化对象时,会在序列化流中附加上codebase的信息。 这个信息告诉接收方到什么地方寻找该对象的执行代码。
你要弄清楚哪个设置codebase,而哪个使用codebase。任何程序假如发送一个对方可能没有的新类对象时就要设置codebase(例如jdk的类对象,就不用设置codebase)。
codebase实际上是一个url表,在该url下有接受方需要下载的类文件。假如你不设置codebase,那么你就不能把一个对象传递给本地没有该对象类文件的程序。
三、怎样设置codebase
在大多数情况下,你可以在命令行上通过属性java.rmi.server.codebase
来设置codebase。例如,假如你在机器url上运行web服务器,端口是8080,你所提供下载的类文件在webserver的根目录下。那么运行应用程序的java 命令行:-Djava.rmi.server.codebase=http://url:8080/
这就是告诉任何接受对象的程序,假如本地没有类文件,可以从这个url下载。
四、类文件应该在什么地方
当接收程序试图从url的webserver上下载代码时,它会把对象的包名转化成目录,到相对于codebase 的该目录下寻找(这点和classpath是一样的)。例如,假如你传递的是类文件yourgroup.project.bean的实例,那么接受方就会到下面的url去下载类文件:
-Djava.rmi.server.codebase=http://url:8080/yourgroup/project/bean.class
一定要保证你把类文件放到webserver根目录下的正确位置,这样这些类文件才能被找到。另一方面,假如你把所有的类文件包装成jar文件,那么设置codebase时就要明确的指出这个jar文件。(这又和 classpath一致了,其实codebase就是网络范围的类路径。)例如你的jar文件是myclasses.jar,那么codebase 如下:
-Djava.rmi.server.codebase=http://url:8080/myclasses.jar
你注重这两种形式的不同。用jar文件后面不用跟‘/’,而用路径的一定用跟‘/’。
Dynamic code downloading using Java RMI
http://docs.oracle.com/javase/7/docs/technotes/guides/rmi/codebase.htmlJava RMI codebase 小议
http://blog.csdn.net/bigtree_3721/article/details/50614289java.rmi.server.codebase Property总结
http://blog.163.com/axuandebin@126/blog/static/11877704200971101837454/
下一篇 NPM使用笔记
页面信息
location:
protocol
: host
: hostname
: origin
: pathname
: href
: document:
referrer
: navigator:
platform
: userAgent
: