java组件中的热插拔(osgi)

 关键字: java 开发 osgi 组件 热插拔

大部分的开发这都是直接使用IDE,很少用人愿意去探究Eclipse内部的情况,而Eclipse本身就是有一大堆的Plug-in组成,同时提供一个OSGi的环境供众多的Plug-in使用。Eclipse与OSGI联姻的行为是从Eclipse 基金在Eclipse 3.0 发布的时候开始的,此后,Eclipse 逐步迁移到OSGi 框架中,并自己实现了一个OSGi 开源框架,取名为Equinox,该框架随着每次Eclipse 的发布也会相应的更新。Eclipse 之所以这么做,其一是因为Eclipse 的插件体系与OSGi 的设计思想不谋而合,其二也是因为OSGi 更为规范,其对插件体系的定义也更为完整一些。事实证明Eclipse 在采用OSGi 架构后,无论从性能、可扩展性这两个方面来讲还是从二次开发的角度来定义,都取得巨大的成功。

从此以后,开发人员不仅能够从网络中获得俯拾皆是的Eclipse Plug-in,并在开发中发挥作用,同时也有很多的团体和个人也参与到了Plug-in的开发过程中。Eclipse Plug-in的最大特点就是热插拔,当然这个特点的源泉便是OSGi。估计很多人都接触过路由器,大部分的路由器都支持模块的热插拔,这就意味着可以在路由器运行的状况下给它动态的增加新的功能或者卸载不需要的功能,硬件界的这种热插拔技术一直就是软件界所追求的,而OSGI则使得热插拔技术在软件界成为现实。基于OSGI的系统,可通过安装新的Bundle、更新或停止现有的Bundle来实现系统功能的插拔。

在这份文档中,我将围绕着OSGi的热插拔技术展开话题。与此同时会介绍OSGi同接口多Bundle服务的使用以及Servlet在OSGi中的使用。同样,为了能够给大家一个感性的认识,我决定还是提供一个小的工程实例来说明我要阐述的问题。这个工程将由一个接口定义Bundle、两个接口实现Bundle以及一个客户端调用Bundle。


1.实现接口定义Bundle

a. 编写代码
 Activator.java

Java代码 复制代码
  1. package org.danlley.osgi.ds.common;   
  2.   
  3. import org.osgi.framework.BundleActivator;   
  4. import org.osgi.framework.BundleContext;   
  5.   
  6. public class Activator implements BundleActivator {   
  7.   
  8.     public void start(BundleContext context) throws Exception {   
  9.     }   
  10.   
  11.     public void stop(BundleContext context) throws Exception {   
  12.     }   
  13.   
  14. }  
package org.danlley.osgi.ds.common;

import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;

public class Activator implements BundleActivator {

	public void start(BundleContext context) throws Exception {
	}

	public void stop(BundleContext context) throws Exception {
	}

}

 

 Item.java

Java代码 复制代码
  1. package org.danlley.osgi.ds.services;   
  2.   
  3. public class Item {   
  4.     String name;   
  5.     String id;   
  6.   
  7.     public Item(String name, String id) {   
  8.         super();   
  9.         this.name = name;   
  10.         this.id = id;   
  11.     }   
  12.   
  13.     public Item() {   
  14.         super();   
  15.     }   
  16.   
  17.     public String getName() {   
  18.         return name;   
  19.     }   
  20.   
  21.     public void setName(String name) {   
  22.         this.name = name;   
  23.     }   
  24.   
  25.     public String getId() {   
  26.         return id;   
  27.     }   
  28.   
  29.     public void setId(String id) {   
  30.         this.id = id;   
  31.     }   
  32. }  
package org.danlley.osgi.ds.services;

public class Item {
	String name;
	String id;

	public Item(String name, String id) {
		super();
		this.name = name;
		this.id = id;
	}

	public Item() {
		super();
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}
}

 

 MySampleListContributor.java 

 

 

Java代码 复制代码
  1. package org.danlley.osgi.ds.services;   
  2.   
  3. import java.util.List;   
  4.   
  5. public interface MySampleListContributor {   
  6.     public List<Item> getItems();   
  7. }  
package org.danlley.osgi.ds.services;

import java.util.List;

public interface MySampleListContributor {
	public List<Item> getItems();
}

 

 

b. 修改配置文件,发布包路径
 
 修改MANIFEST.MF配置文件的内容,添加内容:Export-Package: org.danlley.osgi.ds.services,为后面的两个Bundle实现提供接口,修改后MANIFEST.MF配置文件的内容如下:
  -------------------------------------------------------
  Manifest-Version: 1.0
  Bundle-ManifestVersion: 2
  Bundle-Name: Osgi_ds_interface Plug-in
  Bundle-SymbolicName: osgi_ds_interface
  Bundle-Version: 1.0.0
  Bundle-ClassPath: target/classes/
  Bundle-Activator: org.danlley.osgi.ds.common.Activator
  Bundle-Vendor: some vendor
  Import-Package: org.osgi.framework;version="1.3.0"
  Export-Package: org.danlley.osgi.ds.services
  -------------------------------------------------------
 修改后build.properties配置文件的内容
  ----------------------------------------
  source.target/classes/ = src/main/java/
  output.target/classes/ = target/classes/
  bin.includes = META-INF/,/
          target/classes/
  ----------------------------------------

 

 

2.定义Bundle接口实现

 a. 实现Bunble:osgi_ds_implements_a
  1). 定义类:MySampleListContributor

Java代码 复制代码
  1. package org.danlley.osgi.ds.impl;   
  2.   
  3. import java.util.ArrayList;   
  4. import java.util.List;   
  5. import org.danlley.osgi.ds.services.Item;   
  6. import org.danlley.osgi.ds.services.MySampleListContributor;   
  7.   
  8. public class ContributeA implements MySampleListContributor{   
  9.     public List<Item> getItems(){   
  10.         List<Item> list=new ArrayList<Item>();   
  11.         list.add(new Item("ContributeA","1"));   
  12.         list.add(new Item("ContributeA","2"));   
  13.         list.add(new Item("ContributeA","3"));   
  14.         list.add(new Item("ContributeA","4"));   
  15.         return list;   
  16.     }   
  17. }  
package org.danlley.osgi.ds.impl;

import java.util.ArrayList;
import java.util.List;
import org.danlley.osgi.ds.services.Item;
import org.danlley.osgi.ds.services.MySampleListContributor;

public class ContributeA implements MySampleListContributor{
	public List<Item> getItems(){
		List<Item> list=new ArrayList<Item>();
		list.add(new Item("ContributeA","1"));
		list.add(new Item("ContributeA","2"));
		list.add(new Item("ContributeA","3"));
		list.add(new Item("ContributeA","4"));
		return list;
	}
}

 

  2). 定义一个新目录OSGI-INF用来存放接口实现的相关配置
  3). 在OSGI-INF目录下定义文件component.xml
   内容如下:

Xml代码 复制代码
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <component name="services">  
  3.     <implementation  
  4.         class="org.danlley.osgi.ds.impl.ContributeA" />  
  5.     <service>  
  6.         <provide  
  7.             interface="org.danlley.osgi.ds.services.MySampleListContributor" />  
  8.     </service>  
  9. </component>  

 

 

   说明:关于OSGi接口与实现的绑定事实上是有两种实现的方式,一种便是利用这里的配置文件的声明来实现其绑定的任务,另外还有一种方式是在程序中直接绑定。我们可以使用OSGi为我们提供的一个工具类org.osgi.framework.BundleContext为我们提供的方法registerService(String className,Object _instance, Dictionary arg)来完成这项工作,具体代码示例如下:

Java代码 复制代码
  1. private ServiceRegistration sr = null;   
  2. public void start(BundleContext context) throws Exception {   
  3.     Hashtable props = new Hashtable();   
  4.     props.put("description""This an long value");   
  5.     sr = context.registerService(SomeClass.class.getName(),SomeClassImpl, props);   
  6. }  
private ServiceRegistration sr = null;
public void start(BundleContext context) throws Exception {
	Hashtable props = new Hashtable();
	props.put("description", "This an long value");
	sr = context.registerService(SomeClass.class.getName(),SomeClassImpl, props);
}

 

  4). 声明OSGI-INF/component.xml

   -----------------------------------------------------
   Manifest-Version: 1.0
   Bundle-ManifestVersion: 2
   Bundle-Name: Osgi_ds_implements_a Plug-in
   Bundle-SymbolicName: osgi_ds_implements_a
   Bundle-Version: 1.0.0
   Bundle-ClassPath: target/classes/
   Bundle-Activator: org.danlley.osgi.ds.common.Activator
   Import-Package: org.osgi.framework;version="1.3.0"
   Require-Bundle: osgi_ds_interface
   Service-Component: OSGI-INF/component.xml
   -----------------------------------------------------

 

 b. 实现Bundle:osgi_ds_implement_b
  1). 定义类:MySampleListContributor

Java代码 复制代码
  1. package org.danlley.osgi.ds.impl;   
  2.   
  3. import java.util.ArrayList;   
  4. import java.util.List;   
  5.   
  6. import org.danlley.osgi.ds.services.Item;   
  7. import org.danlley.osgi.ds.services.MySampleListContributor;   
  8.   
  9. public class ContributeB implements MySampleListContributor{   
  10.     public List<Item> getItems(){   
  11.         List<Item> list=new ArrayList<Item>();   
  12.         list.add(new Item("ContributeB","[1]"));   
  13.         list.add(new Item("ContributeB","[2]"));   
  14.         return list;   
  15.     }   
  16. }  
package org.danlley.osgi.ds.impl;

import java.util.ArrayList;
import java.util.List;

import org.danlley.osgi.ds.services.Item;
import org.danlley.osgi.ds.services.MySampleListContributor;

public class ContributeB implements MySampleListContributor{
	public List<Item> getItems(){
		List<Item> list=new ArrayList<Item>();
		list.add(new Item("ContributeB","[1]"));
		list.add(new Item("ContributeB","[2]"));
		return list;
	}
}

 

  2). 定义一个新目录OSGI-INF用来存放接口实现的相关配置
  3). 在OSGI-INF目录下定义文件component.xml
   内容如下:

Java代码 复制代码
  1. <?xml version="1.0" encoding="UTF-8"?>   
  2. <component name="services">   
  3.     <implementation   
  4.         class="org.danlley.osgi.ds.impl.ContributeB" />   
  5.     <service>   
  6.         <provide   
  7.             interface="org.danlley.osgi.ds.services.MySampleListContributor" />   
  8.     </service>   
  9. </component>  
<?xml version="1.0" encoding="UTF-8"?>
<component name="services">
	<implementation
		class="org.danlley.osgi.ds.impl.ContributeB" />
	<service>
		<provide
			interface="org.danlley.osgi.ds.services.MySampleListContributor" />
	</service>
</component>

 

  4). 声明OSGI-INF/component.xml

   -----------------------------------------------------
   Manifest-Version: 1.0
   Bundle-ManifestVersion: 2
   Bundle-Name: Osgi_ds_implement_b Plug-in
   Bundle-SymbolicName: osgi_ds_implement_b
   Bundle-Version: 1.0.0
   Bundle-ClassPath: target/classes/
   Bundle-Activator: org.danlley.osgi.ds.common.Activator
   Bundle-Vendor: some vendor
   Import-Package: org.osgi.framework;version="1.3.0"
   Require-Bundle: osgi_ds_interface
   Service-Component: OSGI-INF/component.xml
   -----------------------------------------------------

 

3. 定义客户端Bundle:osgi_ds_client
 说明:在osgi_ds_client中,我们需要处理两件事情
  i.  分别从两个实现接口的Bundle中获取数据
  ii. 注册一个Servlet用来在浏览器中展现获取的数据
 为此,我们需要设计一个工具类MenuHelper来获取数据,并由ClientServlet来进行数据展现。
 a. 编写代码
  MenuHelper.java

Java代码 复制代码
  1. package org.danlley.osgi.ds.client;   
  2.   
  3. import java.util.ArrayList;   
  4. import java.util.List;   
  5. import org.danlley.osgi.ds.services.MySampleListContributor;   
  6. import org.osgi.service.component.ComponentContext;   
  7.   
  8. public class MenuHelper {   
  9.   
  10.     static MenuHelper instance;   
  11.   
  12.     List<MySampleListContributor> menuContributors = new ArrayList<MySampleListContributor>();   
  13.   
  14.     protected void activate(ComponentContext context) {   
  15.         System.out.println("Calling activate");   
  16.         Object obj = null;   
  17.         try {   
  18.             obj = context.locateService("menuHelper");   
  19.         } catch (Exception e) {   
  20.             e.printStackTrace();   
  21.         }   
  22.         System.out.println(obj);   
  23.         instance = this;   
  24.     }   
  25.   
  26.     protected void deactivate(ComponentContext context) throws Exception {   
  27.         System.out.println("Calling deactivate");   
  28.     }   
  29.   
  30.     public static MenuHelper getInstance() {   
  31.         return instance;   
  32.     }   
  33.   
  34.     public List<MySampleListContributor> getMenuContributors() {   
  35.         return menuContributors;   
  36.     }   
  37.   
  38.     public void addMenuContributor(MySampleListContributor menuContributor) {   
  39.         System.out.println("Calling addMenuContributor");   
  40.         menuContributors.add(menuContributor);   
  41.     }   
  42.   
  43.     public void removeMenuContributor(MySampleListContributor menuContributor) {   
  44.         System.out.println("Calling removeMenuContributor");   
  45.         menuContributors.remove(menuContributor);   
  46.     }   
  47. }  
package org.danlley.osgi.ds.client;

import java.util.ArrayList;
import java.util.List;
import org.danlley.osgi.ds.services.MySampleListContributor;
import org.osgi.service.component.ComponentContext;

public class MenuHelper {

	static MenuHelper instance;

	List<MySampleListContributor> menuContributors = new ArrayList<MySampleListContributor>();

	protected void activate(ComponentContext context) {
		System.out.println("Calling activate");
		Object obj = null;
		try {
			obj = context.locateService("menuHelper");
		} catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println(obj);
		instance = this;
	}

	protected void deactivate(ComponentContext context) throws Exception {
		System.out.println("Calling deactivate");
	}

	public static MenuHelper getInstance() {
		return instance;
	}

	public List<MySampleListContributor> getMenuContributors() {
		return menuContributors;
	}

	public void addMenuContributor(MySampleListContributor menuContributor) {
		System.out.println("Calling addMenuContributor");
		menuContributors.add(menuContributor);
	}

	public void removeMenuContributor(MySampleListContributor menuContributor) {
		System.out.println("Calling removeMenuContributor");
		menuContributors.remove(menuContributor);
	}
}

 

  ClientServlet.java

 

Java代码 复制代码
  1. package org.danlley.osgi.ds.client.servlet;   
  2.   
  3. import java.io.IOException;   
  4. import javax.servlet.http.HttpServlet;   
  5. import javax.servlet.http.HttpServletRequest;   
  6. import javax.servlet.http.HttpServletResponse;   
  7. import org.danlley.osgi.ds.client.MenuHelper;   
  8. import org.danlley.osgi.ds.services.Item;   
  9. import org.danlley.osgi.ds.services.MySampleListContributor;   
  10.   
  11. public class ClientServlet extends HttpServlet {   
  12.     /**  
  13.      * serialVersionUID  
  14.      */  
  15.     private static final long serialVersionUID = -2631550134006854279L;   
  16.   
  17.     public void doPost(HttpServletRequest servlet, HttpServletResponse response) throws IOException {   
  18.         doGet(servlet, response);   
  19.     }   
  20.   
  21.     public void doGet(HttpServletRequest servlet, HttpServletResponse response) throws IOException {   
  22.         response.setContentType("text/html");   
  23.         response.getWriter().write("Hello  
  24. ");   
  25.         response.getWriter().write("<ul>");   
  26.         for (MySampleListContributor mycontribute : MenuHelper.getInstance().getMenuContributors()) {   
  27.             for (Item mItem : mycontribute.getItems()) {   
  28.                 response.getWriter().write(   
  29.                     "<li><a href=/"" + mItem.getId() + "/">" + mItem.getName() + "[" +    
  30.                     mItem.getId() + "]" + "</a></li>");   
  31.             }   
  32.         }   
  33.         response.getWriter().write("</ul>");   
  34.     }   
  35.   
  36. }  
package org.danlley.osgi.ds.client.servlet;

import java.io.IOException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.danlley.osgi.ds.client.MenuHelper;
import org.danlley.osgi.ds.services.Item;
import org.danlley.osgi.ds.services.MySampleListContributor;

public class ClientServlet extends HttpServlet {
	/**
	 * serialVersionUID
	 */
	private static final long serialVersionUID = -2631550134006854279L;

	public void doPost(HttpServletRequest servlet, HttpServletResponse response) throws IOException {
		doGet(servlet, response);
	}

	public void doGet(HttpServletRequest servlet, HttpServletResponse response) throws IOException {
		response.setContentType("text/html");
		response.getWriter().write("Hello<br/>");
		response.getWriter().write("<ul>");
		for (MySampleListContributor mycontribute : MenuHelper.getInstance().getMenuContributors()) {
			for (Item mItem : mycontribute.getItems()) {
				response.getWriter().write(
					"<li><a href=/"" + mItem.getId() + "/">" + mItem.getName() + "[" + 
					mItem.getId() + "]" + "</a></li>");
			}
		}
		response.getWriter().write("</ul>");
	}

}

 

 b.注册Servlet
  在工程根目录定义文件plugin.xml,同时也可以采用在Eclipse提供的可视化界面进行操作。

Xml代码 复制代码
  1. <plugin>  
  2.   <extension point="org.eclipse.equinox.http.registry.resources">  
  3.     <resource  
  4.       alias="/web"  
  5.       base-name="/page"/>  
  6.   </extension>  
  7.      
  8.     <extension point="org.eclipse.equinox.http.registry.servlets">  
  9.         <servlet  
  10.           alias="/exampleServlet"  
  11.           class="org.danlley.osgi.ds.client.servlet.ClientServlet"/>  
  12.     </extension>  
  13. </plugin>  

 

 c. 定义文件目录OSGI-INF
 d. 在OSGI-INF中增加文件component.xml

Xml代码 复制代码
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <component name="menuHelper">  
  3.     <implementation     
  4.         class="org.danlley.osgi.ds.client.MenuHelper"/>      
  5.     <reference name="menuHelper"  
  6.         interface="org.danlley.osgi.ds.services.MySampleListContributor"  
  7.         cardinality="0..n"  
  8.         policy="dynamic"  
  9.         bind="addMenuContributor"  
  10.         unbind="removeMenuContributor"  
  11.     />  
  12. </component>  

 

  这样,在系统初始化过程中,我们定义在MenuHelper中的activate方法首先会被执行,然后,locateService()查找系统中是否有可用的"menuHelper",接着查找MySampleListContributor实例,并由addMenuContributor方法完成合并工作。另外,当我们停掉客户端调用Bundle时,deactivate方法会在整个Bundle被Stop之前被调用来执行一些清理工作。
 e. 修改配置文件

  ------------------------------------------------------
  Manifest-Version: 1.0
  Bundle-ManifestVersion: 2
  Bundle-Name: Osgi_ds_client Plug-in
  Bundle-SymbolicName: osgi_ds_client;singleton:=true
  Bundle-Version: 1.0.0
  Bundle-ClassPath: target/classes/
  Bundle-Activator: org.danlleyo.osgi.ds.common.Activator
  Import-Package: org.osgi.framework;version="1.3.0"
  Service-Component: OSGI-INF/component.xml
  Require-Bundle: osgi_ds_interface,
   org.eclipse.osgi,
   org.eclipse.osgi.services,
   org.eclipse.equinox.http.registry,
   org.eclipse.equinox.common,
   org.eclipse.equinox.launcher,
   org.eclipse.equinox.http.servlet,
   org.eclipse.equinox.ds,
   org.eclipse.equinox.http.helper,
   org.eclipse.equinox.http.jetty,
   org.eclipse.core.jobs,
   org.apache.commons.logging,
   javax.servlet,
   org.mortbay.jetty,
   osgi_ds_implement_b,
   osgi_ds_implements_a
  ------------------------------------------------------

 

4. 系统运行
 a. 启动服务:
  运行结果如下,说明服务成功启动。
  ---------------------------------------------------------------
  osgi> Jul 23, 2008 2:08:58 PM org.mortbay.http.HttpServer doStart
  INFO: Version Jetty/5.1.x
  Jul 23, 2008 2:08:58 PM org.mortbay.util.Container start
  INFO: Started org.mortbay.jetty.servlet.ServletHandler@1081d2e
  Jul 23, 2008 2:08:58 PM org.mortbay.util.Container start
  INFO: Started HttpContext[/,/]
  Jul 23, 2008 2:08:58 PM org.mortbay.http.SocketListener start
  INFO: Started SocketListener on 0.0.0.0:80
  Jul 23, 2008 2:08:58 PM org.mortbay.util.Container start
  INFO: Started org.mortbay.http.HttpServer@e0b6f5
  Calling addMenuContributor
  Calling addMenuContributor
  Calling activate
  org.danlley.osgi.ds.impl.ContributeA@c44b88
  ---------------------------------------------------------------
 b. 访问服务
  访问地址:http://localhost/exampleServlet 如果出现6条数据(4条ContributeA的数据和2条ContributeB的数据),则说明系统运行正常。
 c. 热插拔Bundle
  查看当前系统中所有运行Bundle的运行状态及ID,结果如下:
  ------------------------------------------------------------------------------
  ss

  Framework is launched.

  id State       Bundle
  0 ACTIVE      org.eclipse.osgi_3.3.2.R33x_v20080105
  1879 ACTIVE      org.eclipse.osgi.services_3.1.200.v20070605
  2304 ACTIVE      javax.servlet_2.4.0.v200706111738
  3059 ACTIVE      org.eclipse.equinox.launcher_1.0.1.R33x_v20080118
  3060 ACTIVE      org.apache.commons.logging_1.0.4
  3065 ACTIVE      org.eclipse.equinox.http.registry_1.0.1.R33x_v20071231
  3066 ACTIVE      org.eclipse.equinox.registry_3.3.1.R33x_v20070802
  3067 ACTIVE      org.eclipse.equinox.common_3.3.0.v20070426
  3074 ACTIVE      org.apache.commons.logging_1.0.4.v200706111724
  3087 ACTIVE      org.eclipse.equinox.http.jetty_1.0.1.R33x_v20070816
  3095 ACTIVE      osgi_ds_interface_1.0.0
  3096 ACTIVE      org.eclipse.equinox.ds_1.0.0.qualifier
  3102 ACTIVE      org.eclipse.equinox.http.helper_1.0.0.qualifier
  3103 ACTIVE      osgi_ds_implement_b_1.0.0
  3104 ACTIVE      osgi_ds_implements_a_1.0.0
  3106 ACTIVE      org.eclipse.core.jobs_3.3.1.R33x_v20070709
  3109 ACTIVE      org.mortbay.jetty_5.1.11.v200706111724
  3112 ACTIVE      org.eclipse.equinox.http.servlet_1.0.1.R33x_v20070816
  3125 ACTIVE      osgi_ds_client_1.0.0
  ------------------------------------------------------------------------------

  执行stop 3104,运行结果如下:
   -----------------------------
   osgi> stop 3104
   Calling removeMenuContributor
   -----------------------------
  此时如果刷新页面,页面上会仅剩两天数据。说明osgi_ds_implements_a已被成功卸载。
  继续执行stop 3103,运行结果如下:
   ------------------------------
   osgi> stop 3103
   Calling removeMenuContributor
   ------------------------------
  此时,刷新页面刚才展示的数据就会全部消失。
  继续执行stop 3125,运行结果如下:
   --------------------
   osgi> stop 3125
   Calling deactivate
   ---------------------
  可见客户端Bundle在停止之前,确实会执行deactivate方法来做一些清理工作。

 

 

 QQ群:9896150 【Java终结者】


 结束!

 

 

参考资料: http://www.springframework.org/

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值