Java WS和二进制数据传送

(一)  有关介绍

二进制数据在WS中传送,可以有两种方式:

1 把数据直接作为xml文档中某元素的字节流,作为XML解析器要解析的一部分,很明显这种方式比较低效.

2 二进制数据作为附件,作为带外(out of band)数据随同XML发送, 提高了效率.目前这类处理有几个规范:

DIME(直接 Internet 消息封装),这个数据包装格式及其处理,只有微软在支持.

http://www.microsoft.com/china/msdn/archives/library/dnwebsrv/html/DIMEWSAttch.asp

http://www.zdnet.com.cn/developer/code/story/0,3800066897,39358789,00.htm

MTOM(SOAP 消息传输优化机制)和XOP(二进制 XML 优化封装);

在Sun规范JAX-RPC1.1中,要求使用SwA(SOAP with Attachments) 支持附件,为此Sun提供了SOAP with Attachments API for Java,带附件的SoapAPI(SAAJ),早期它和jaxm合在一起的,现在已经独立开来形成了soap包,这个API专门用来处理Soap附件的所有操作.

JAXRPC 1.1 规范定义了MIME类型到Java类型的影射.

MIME Type
Java Type
image/gifjava.awt.Image
image/jpegjava.awt.Image
text/plainjava.lang.String
multipart/*javax.mail.internet.MimeMultipart
text/xml or application/xmljavax.xml.transform.Source

JAXRPC 1.1 规范定义了这种情况:当绑定到上表中没有定义的类型影射或者是绑定到备用MIME类型时,它应该影射到 javax.activation.DataHandler.

wscompile工具中的选项-f:<features>用于类型影射的项:datahandleronly,该项指明总是把附件影射到DataHandler类型.

(二)  编写例子应用

文件清单

SEI接口类IImage.java,实现SEI接口的类IIMageImpl.java,描述文件信息的值类型类FileInfo.java,Web应用部署描述文件web.xml,WS发布配置文件jaxrpc-ri.xml,WS编译配置文件config-interface.xml,构建客户端桩的config-wsdl.xml,构建文件build.xml

客户端测试类:MainBrowser.java,ImageListProvider.java和ImageLabelProvider.java,测试使用了SWT和JFace,请适当配置你的Eclipse环境,引入适当的库.

这个例子是从SEI开始,绑定样式为 RPC.

IImage.java清单: 

package com.bin;

import java.rmi.Remote;
import java.rmi.RemoteException;
import javax.activation.*;
import javax.xml.soap.*;
import java.util.*;
public interface IImage extends Remote {

 public DataHandler fetchImg(String sn) throws RemoteException;

 public ArrayList fetchImgs(String[] sn) throws RemoteException;

 public SOAPMessage construcMsg(String[] fn) throws RemoteException;

 public ArrayList fetchFileList() throws java.rmi.RemoteException;
 
 public FileInfo getFileList(String fn) throws java.rmi.RemoteException;
}

实现类 ImageImpl.java清单:

package com.bin;

import javax.xml.soap.*;
import java.net.*;
import java.util.*;
import java.io.*;
import javax.activation.*;
import java.rmi.*;
import java.awt.*;
import javax.xml.rpc.ServiceException;
import javax.xml.rpc.server.*;
import javax.xml.rpc.handler.soap.SOAPMessageContext;
import javax.xml.rpc.handler.MessageContext;
import javax.servlet.ServletContext;
import com.sun.xml.rpc.server.*;

public class ImageImpl implements IImage, ServiceLifecycle {

 ServletEndpointContext servletEndpointContext = null;

 String binarypath = "";

 ArrayList al;

 ServletContext servletContext = null;

 public void init(Object p0) throws ServiceException {
  // Some logic to do upon service creation
  servletEndpointContext = (ServletEndpointContext) p0;
  servletContext = servletEndpointContext.getServletContext();
  binarypath = servletContext.getInitParameter("BinaryPath");
 }

 public void destroy() {
  // Some logic to do on service destruction - e.g. clean up JDBC
  servletEndpointContext = null;
  servletContext = null;
 }

 public ArrayList fetchFileList() throws RemoteException {
  File file = new File(this.binarypath);
  //System.out.println(this.binarypath);
  if (al == null)
   al = new ArrayList();
  File[] c = file.listFiles();
  for (int i = 0; i < c.length; i++) {
   FileInfo fi = new FileInfo();
   fi.setIsdir(c[i].isDirectory());
   fi.setFilename(c[i].getName());
   fi.setFilelength(c[i].length());
   fi.setFilepath(c[i].getAbsolutePath());
   fi.setCreatedate(new Date(c[i].lastModified()));
   al.add(fi);
  }
  return al;
 }

 public DataHandler fetchImg(String sn) throws RemoteException {
  File file = new File(sn);
  DataHandler dataHandler = null;
  try {
   URL url = new URL(file.toURL().toString());
   dataHandler = new DataHandler(url);
  } catch (Exception ex) {
   System.out.println(ex);
   throw new RemoteException(ex.getMessage());
  }
  return dataHandler;
 }

 public ArrayList fetchImgs(String[] fn) throws RemoteException {
  ArrayList al = new ArrayList();
  try {
   for (int i = 0; i < fn.length; i++) {
    File file = new File(fn[i]);
    // Create attachment part for image
    URL url = new URL(file.toURL().toString());
    DataHandler dataHandler = new DataHandler(url);
    al.add(dataHandler);
   }
  } catch (Exception ex) {
   System.out.println(ex);
   throw new RemoteException(ex.getMessage());
  }
  return al;
 }

 public SOAPMessage construcMsg(String[] fn) throws RemoteException {
  FileReader fr = null;
  BufferedReader br = null;
  String line = "";
  SOAPMessage message = null;
  try {
   // Create message factory
   MessageFactory messageFactory = MessageFactory.newInstance();
   // Create a message
   message = messageFactory.createMessage();

   // Get the SOAP header and body from the message
   // and remove the header
   SOAPHeader header = message.getSOAPHeader();
   SOAPBody body = message.getSOAPBody();
   header.detachNode();
   for (int i = 0; i < fn.length; i++) {
    File file = new File(fn[i]);
    // Create attachment part for image
    URL url = new URL(file.toURL().toString());
    DataHandler dataHandler = new DataHandler(url);
    AttachmentPart attachment2 = message
      .createAttachmentPart(dataHandler);
    attachment2.setContentId(file.getName());
    message.addAttachmentPart(attachment2);
   }
  } catch (IOException e) {
   System.out.println("I/O exception: " + e.toString());
   throw new RemoteException(e.getMessage());
  } catch (Exception ex) {
   ex.printStackTrace();
   throw new RemoteException(ex.getMessage());
  }
  return message;
 }

 public FileInfo getFileList(String fn) throws RemoteException {
  // TODO Auto-generated method stub
  File file = new File(fn);
  FileInfo fi = new FileInfo();
  fi.setFilename(file.getName());
  fi.setFilelength(file.length());
  fi.setFilepath(file.getAbsolutePath());
  fi.setCreatedate(new Date(file.lastModified()));
  return fi;
 }
}

FileInfo.java清单:

package com.bin;

import java.util.Date;

/**
 * 这是一个值类型,既然是值类型,它满足:

1 必须有缺省构造器. 2 必须没有实现Remote(直接的或是间接的). 3属性类型必须是JAX-RPC支持的类型.
 */
public class FileInfo {
 private String filename;

 private String filepath;

 private long filelength;

 private Date createdate;

 private boolean isdir = false;

 public FileInfo() {
 }

 public Date getCreatedate() {
  return createdate;
 }

 public void setCreatedate(Date createdate) {
  this.createdate = createdate;
 }

 public long getFilelength() {
  return filelength;
 }

 public void setFilelength(long filelength) {
  this.filelength = filelength;
 }

 public String getFilename() {
  return filename;
 }

 public void setFilename(String filename) {
  this.filename = filename;
 }

 public String getFilepath() {
  return filepath;
 }

 public void setFilepath(String filepath) {
  this.filepath = filepath;
 }

 public String toString() {
  return this.filename + this.getFilepath() + this.getFilelength();
 }

 public boolean isIsdir() {
  return isdir;
 }

 public void setIsdir(boolean isdir) {
  this.isdir = isdir;
 }
}

web.xml清单:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
 version="2.4">
 <context-param>
  <param-name>BinaryPath</param-name>
  <param-value>更改成你的本地文件路径</param-value>
 </context-param>
 <welcome-file-list>
  <welcome-file>index.htm</welcome-file>
  <welcome-file>index.jsp</welcome-file>
  <welcome-file>index.jws</welcome-file>
 </welcome-file-list>
</web-app>

config-interface.xml清单:

<?xml version="1.0" encoding="UTF-8"?>
<configuration xmlns="http://java.sun.com/xml/ns/jax-rpc/ri/config">
 <service name="BinaryService" targetNamespace="urn:binary"
  typeNamespace="urn:binary" packageName="com.binary">
  <interface name="com.bin.IImage"
   servantName="com.bin.ImageImpl" />
 </service>
</configuration>

config-wsdl.xml清单:

<?xml version="1.0" encoding="UTF-8"?>
<configuration xmlns="http://java.sun.com/xml/ns/jax-rpc/ri/config">
 <wsdl location="http://localhost:8080/skysoft/binary?WSDL"
  packageName="com.binary" />
</configuration>

jaxrpc-ri.xml清单:

<?xml version="1.0" encoding="UTF-8"?>
<webServices xmlns="http://java.sun.com/xml/ns/jax-rpc/ri/dd"
 version="1.0"
 targetNamespaceBase="http://java.sun.com/xml/ns/jax-rpc/wsi/wsdl"
 typeNamespaceBase="http://java.sun.com/xml/ns/jax-rpc/wsi/types"
 urlPatternBase="/ws">
 <endpoint name="binaryservice" displayName="Stock Example"
  description="Binary Web Service endpoint"
  interface="com.bin.IImage" implementation="com.bin.ImageImpl"
  model="/WEB-INF/model.xml.gz" />
 <endpointMapping endpointName="binaryservice" urlPattern="/binary" />
</webServices>

构建文件build.xml:依次运行build,create-war,deploy,genstaticstub等任务,这样本例中的WS所需要的文件全部生成.

<?xml version="1.0" encoding="GBK"?>
<project name="webservice" default="build" basedir=".">
 <property name="jaxrpc.lib.dir2" value="D:/jwsdp-1.5/jaxrpc/lib">
 </property>
 <property name="jaxrpc.lib.dir" value="I:/jwsdp-1.6/jaxrpc/lib">
 </property>
 <property name="jaxrpc.lib.dir1" value="D:/Sun/AppServer/lib">
 </property>
 <property name="classes.dir" value="./build/classes">
 </property>
 <property name="src.dir" value="./build/src">
 </property>
 <property name="war.file" value="hello_raw.war">
 </property>
 <property name="mapping.file" value="mapping.xml">
 </property>

 <property name="tmp.dir" value="./tmp">
 </property>
 <property name="nonclass.dir" value="./build/nonclass">
 </property>
 <property name="build" value="${nonclass.dir}">
 </property>
 <property name="assemble" value="./assemble">
 </property>
 <property name="assemble.war" value="./assemble/war">
 </property>
 <property name="assemble.ear" value="./assemble/ear">
 </property>

 <path id="jaxrpc-classpath">
  <!--fileset dir="${jaxrpc.lib.dir}">
   <include name="**/*.jar" />
  </fileset-->
  <fileset dir="D:/jdbc/postgresql">
   <include name="*.jar" />
  </fileset>
  <fileset dir="I:/jwsdp-1.6/saaj/lib">
   <include name="*.jar" />
  </fileset>
  <fileset dir="I:/jwsdp-1.6/jaxrpc/lib">
   <include name="*.jar" />
  </fileset>
  <fileset dir="I:/jwsdp-1.6/jwsdp-shared/lib">
   <include name="**/*.jar" />
  </fileset>
 </path>
 <path id="compile.classpath">
  <!--fileset dir="${jaxrpc.lib.dir}">
   <include name="**/*.jar" />
  </fileset-->
  <fileset dir="I:/jwsdp-1.6/jwsdp-shared/lib">
   <include name="**/*.jar" />
  </fileset>
  <fileset dir="i:/jwsdp-1.6/jaxrpc/lib">
   <include name="**/*.jar" />
  </fileset>
  <fileset dir="I:/jwsdp-1.6/saaj/lib">
   <include name="*.jar" />
  </fileset>
 </path>

 <taskdef name="wscompile" classpathref="jaxrpc-classpath" classname="com.sun.xml.rpc.tools.ant.Wscompile">
 </taskdef>
 <taskdef name="wsdeploy" classpathref="jaxrpc-classpath" classname="com.sun.xml.rpc.tools.ant.Wsdeploy">
 </taskdef>
 <target name="prepare">
  <mkdir dir="${src.dir}" />
  <mkdir dir="${nonclass.dir}" />
  <mkdir dir="${classes.dir}" />
  <mkdir dir="${assemble}" />
  <mkdir dir="${assemble.war}" />
  <mkdir dir="${assemble.ear}" />
  <mkdir dir="${tmp.dir}" />
 </target>
 <target name="help" description="generates ws classes">
  <echo message="help ........" />
  <wscompile help="true">
   <classpath refid="compile.classpath" />
  </wscompile>
 </target>

 <target name="build" depends="prepare" description="generates ws classes">
  <echo message="build the WAR...." />
  <wscompile import="false" define="true" gen="false" features="wsi,documentliteral" keep="true" base="${classes.dir}" sourceBase="${src.dir}" classpath="./classes" nonClassDir="${nonclass.dir}" model="model.xml.gz" xPrintStackTrace="true" config="config-interface.xml" verbose="true">
   <classpath refid="compile.classpath" />
  </wscompile>
 </target>

 <target name="deploy" description="deploy ws">
  <echo message="deploy the WAR...." />
  <wsdeploy keep="false" verbose="true" tmpDir="${tmp.dir}" outWarFile="skysoft.war" inWarFile="hello_raw.war">
   <classpath refid="compile.classpath" />
  </wsdeploy>
 </target>
 <target name="create-war" description="Packages the WAR file">
  <echo message="Creating the WAR...." />
  <delete file="${assemble.war}/${war.file}" />
  <delete dir="${assemble.war}/WEB-INF" />
  <copy todir="${assemble.war}/WEB-INF/classes/">
   <fileset dir="./classes" includes="**/*.class" excludes="**/*Client.class, **/*.wsdl, **/*mapping.xml" />
  </copy>
  <copy todir="${assemble.war}/WEB-INF/lib/">
   <fileset dir="./lib" includes="**/*.jar" excludes="**/*Client.class, **/*.wsdl, **/*mapping.xml" />
  </copy>
  <copy file="jaxrpc-ri.xml" todir="${assemble.war}/WEB-INF" />
  <copy file="model.xml.gz" todir="${assemble.war}/WEB-INF" />
  <war destfile="${assemble.war}/${war.file}" webxml="./web.xml" filesonly="true">
   <fileset dir="${assemble.war}" includes="WEB-INF/**, build/**" />
  </war>
  <copy file="${assemble.war}/${war.file}" todir="." />
 </target>
 <target name="genstaticstub">
  <echo message="gen statics tub" />
  <wscompile client="true" keep="true" base="./staticstub" sourceBase="./staticstub" xPrintStackTrace="true" config="config-wsdl.xml" verbose="true">
   <classpath refid="compile.classpath" />
  </wscompile>
 </target>
</project>

接下来,发布skysoft.war到你的web服务环境,本例使用的是Tomcat5,检查http://localhost:8080/skysoft/binary?WSDL是否装入.
发布成功后,用Eclipse新建一个java项目,把静态桩文件悉数拷贝到该项目的src下,然后编写以下类:

MainBrowser窗口类,显示图文件.

package swtui;

import java.io.IOException;
import java.rmi.RemoteException;
import java.util.ArrayList;

import javax.xml.rpc.Stub;

import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.window.ApplicationWindow;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.widgets.*;

import com.binary.BinaryService_Impl;
import com.binary.FileInfo;
import com.binary.IImage;
import com.binary.IImage_Stub;

public class MainBrowser extends ApplicationWindow {
 Label label;

 ImageLabelProvider canvas;

 Image image;

 public MainBrowser(Shell arg0) {
  super(arg0);
  // TODO Auto-generated constructor stub
 }

 public MainBrowser() {
  super(null);
  addStatusLine();
  // TODO Auto-generated constructor stub
 }

 /**
  * ImageLoader Currently supported image formats are:
  *
  * BMP (Windows Bitmap) ICO (Windows Icon) JPEG GIF PNG
  */
 protected Control createContents(Composite parent) {
  getShell().setText("JFace File Explorer");
  SashForm sash_form = new SashForm(parent, SWT.HORIZONTAL | SWT.NULL);
  TableViewer tbv = new TableViewer(sash_form, SWT.BORDER
    | SWT.FULL_SELECTION | SWT.MULTI);
  ImageListProvider img = new ImageListProvider();
  tbv.setContentProvider(img);
  tbv.setInput(getFileInfo());

  tbv.addSelectionChangedListener(new ISelectionChangedListener() {
   public void selectionChanged(SelectionChangedEvent event) {
    IStructuredSelection selection = (IStructuredSelection) event
      .getSelection();
    FileInfo fi = (FileInfo) selection.getFirstElement();
    if (!fi.isIsdir()) {
     showImage(fi.toString());
     canvas.adjustSize();
     // canvas.redraw();
    }
    setStatus(fi.toString());
   }
  });

  canvas = new ImageLabelProvider(sash_form, SWT.SHELL_TRIM
    | SWT.NO_BACKGROUND | SWT.NO_REDRAW_RESIZE | SWT.V_SCROLL
    | SWT.H_SCROLL | SWT.CENTER);

  return sash_form;
 }

 public static void main(String[] args) {
  MainBrowser w = new MainBrowser();
  w.setBlockOnOpen(true);
  w.open();
 }

 public void showImage(String fn) {
  IImage_Stub stub = (IImage_Stub) createProxy();
  IImage hello = (IImage) stub;

  ImageData idata = null;
  try {
   idata = new ImageData(hello.fetchImg(fn).getInputStream());
  } catch (RemoteException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } catch (IOException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  ImageDescriptor id = ImageDescriptor.createFromImageData(idata);
  if (image != null)
   image.dispose();
  image = id.createImage();
  // label.setImage(id.createImage());
  // canvas.setData(image);
  canvas.setImage(image);
 }

 public Object getFileInfo() {
  IImage_Stub stub = (IImage_Stub) createProxy();
  IImage hello = (IImage) stub;
  ArrayList al = null;
  try {
   al = hello.fetchFileList();
  } catch (RemoteException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  return al;
 }

 private Stub createProxy() {
  return (Stub) (new BinaryService_Impl().getIImagePort());
 }
}


ImageLabelProvider标签类,能滚动图象的组件.

package swtui;

import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.ScrollBar;
import org.eclipse.swt.*;

public class ImageLabelProvider extends Canvas {
 ScrollBar hBar = null;

 ScrollBar vBar = null;

 private Image image;

 public Image getImage() {
  return image;
 }

 public void setImage(Image image) {
  this.image = image;
  setData(image);
 }

 public ImageLabelProvider() {
  this(null, SWT.CENTER);
  // TODO Auto-generated constructor stub
 }

 public ImageLabelProvider(Composite arg0, int arg1) {
  super(arg0, arg1);
  hBar = getHorizontalBar();
  vBar = getVerticalBar();
  setListener();
  // TODO Auto-generated constructor stub
 }

 public void adjustSize() {
  if(image==null)
   return;
  final Point origin = new Point(0, 0);
  Rectangle rect = image.getBounds();
  Rectangle client = getClientArea();
  hBar.setMaximum(rect.width);
  vBar.setMaximum(rect.height);
  hBar.setThumb(Math.min(rect.width, client.width));
  vBar.setThumb(Math.min(rect.height, client.height));
  int hPage = rect.width - client.width;
  int vPage = rect.height - client.height;
  int hSelection = hBar.getSelection();
  int vSelection = vBar.getSelection();
  if (hSelection >= hPage) {
   if (hPage <= 0)
    hSelection = 0;
   origin.x = -hSelection;
  }
  if (vSelection >= vPage) {
   if (vPage <= 0)
    vSelection = 0;
   origin.y = -vSelection;
  }
  redraw();
 }

 private void setListener() {
  final Point origin = new Point(0, 0);

  hBar.addListener(SWT.Selection, new Listener() {
   public void handleEvent(Event e) {
    int hSelection = hBar.getSelection();
    int destX = -hSelection - origin.x;
    Rectangle rect = image.getBounds();
    scroll(destX, 0, 0, 0, rect.width, rect.height, false);
    origin.x = -hSelection;
   }
  });
  vBar.addListener(SWT.Selection, new Listener() {
   public void handleEvent(Event e) {
    int vSelection = vBar.getSelection();
    int destY = -vSelection - origin.y;
    Rectangle rect = getBounds();
    scroll(0, destY, 0, 0, rect.width, rect.height, false);
    origin.y = -vSelection;
   }
  });
  addListener(SWT.Resize, new Listener() {
   public void handleEvent(Event e) {
    Rectangle rect = image.getBounds();
    Rectangle client = getClientArea();
    hBar.setMaximum(rect.width);
    vBar.setMaximum(rect.height);
    hBar.setThumb(Math.min(rect.width, client.width));
    vBar.setThumb(Math.min(rect.height, client.height));
    int hPage = rect.width - client.width;
    int vPage = rect.height - client.height;
    int hSelection = hBar.getSelection();
    int vSelection = vBar.getSelection();
    if (hSelection >= hPage) {
     if (hPage <= 0)
      hSelection = 0;
     origin.x = -hSelection;
    }
    if (vSelection >= vPage) {
     if (vPage <= 0)
      vSelection = 0;
     origin.y = -vSelection;
    }
    redraw();
   }
  });
  addListener(SWT.Paint, new Listener() {
   public void handleEvent(Event e) {
    GC gc = e.gc;
    gc.drawImage(image, origin.x, origin.y);
    Rectangle rect = image.getBounds();
    Rectangle client = getClientArea();
    int marginWidth = client.width - rect.width;
    if (marginWidth > 0) {
     gc.fillRectangle(rect.width, 0, marginWidth, client.height);
    }
    int marginHeight = client.height - rect.height;
    if (marginHeight > 0) {
     gc.fillRectangle(0, rect.height, client.width,
         marginHeight);
    }
   }
  });
 }
}

ImageListProvider.java代码清单:

package swtui;

import java.io.File;
import java.rmi.RemoteException;
import java.util.ArrayList;

import javax.xml.rpc.Stub;

import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.Viewer;
import com.binary.*;
public class ImageListProvider  implements IStructuredContentProvider{

 public Object[] getElements1(Object element) {
  Object[] kids =  ((ArrayList) element).toArray();
  return kids == null ? new Object[0] : kids;
 }

 private Stub createProxy() {
  return (Stub) (new BinaryService_Impl().getIImagePort());
 }

 public void dispose() {
 }

 public void inputChanged(Viewer arg0, Object arg1, Object arg2) {
 }

}

到此,例子结束,应该说使用Eclipse,借助于插件,开发一个web服务还是相当快捷的. 本例可通过扩展客户端以处理其它格式的文件.

下一篇将介绍如何在C#中调用这个服务.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值