webservice,搞懂它

一. 简介

一、WebService是什么

WebService是一种跨编程语言和跨操作系统平台的远程调用技术

跨编程语言:就是说服务端程序采用java编写,客户端程序则可以采用其他编程语言编写,反之亦然!

跨操作系统平台:服务端程序和客户端程序可以在不同的操作系统上运行。

远程调用:就是一台计算机a上的一个程序可以调用到另外一台计算机b上的一个程序的接口方法,譬如,银联提供给商场的pos刷卡系统,商场的POS机转账调用的转账方法的代码其实是跑在银行服务器上。再比如,amazon,天气预报系统,淘宝网,校内网,百度等把自己的系统服务以webservice服务的形式暴露出来,让第三方网站和程序可以调用这些服务功能,这样扩展了自己系统的市场占有率,往大的概念上吹,就是所谓的SOA应用,当然,webservice也是rpc远程调用的一种实现方式。

二、为什么要用Web service?

  web service能解决:

  1. 跨平台调用

  2. 跨语言调用

  3. 远程调用

三、什么时候使用web Service?

  1. 同一家公司的新旧应用之间

  2. 不同公司的应用之间

  3. 一些提供数据的内容聚合应用:天气预报、股票行情

四、Web Service中的几个重要术语

4.1、WSDL(web service definition language)

  WSDL是webservice定义语言, 对应.wsdl文档, 一个webservice会对应一个唯一的wsdl文档, 定义了客户端与服务端发送请求和响应的数据格式和过程

4.2、SOAP(simple object  access protocal)

  SOAP是"简单对象访问协议"

  1. 是一种简单的、基于HTTPXML的协议, 用于在WEB上交换结构化的数据

  2. soap消息:请求消息响应消息

4.3、SEI(WebService EndPoint Interface)

  SEI是web service的终端接口,就是WebService服务器端用来处理请求的接口

4.4、CXF(Celtix + XFire)

  一个apache的用于开发webservice服务器端和客户端的框架。

二、一步步使用jdk实现webservice

webservice服务提供方

一、新建一个java project,命名为ceshi_webservice_server,用来当做webservice提供服务的一端

二、编写接口、接口的实现类、服务发布类,结构如图:

image.png

可以看到,使用jdk发布webservice不需要引入任何的jar包。

这三个类结构都很简单,如下。

IpManage.java是接口类,表明了你这个webservice要提供哪些服务给外部,代码如下:

package cn.zhao.svc;

import javax.jws.WebMethod;
import javax.jws.WebService;
@WebService
public interface IpManage {
	@WebMethod
	public String auth(byte[] ip);
}

很简单,只有一个方法,接受一个字节数组,返回一个字符串。

IpManageImpl是IpManage的实现类,代码如下:

package cn.zhao.svc.impl;

import javax.jws.WebService;

import cn.zhao.svc.IpManage;

@WebService
public class IpManageImpl implements IpManage {
	@Override
	public String auth(byte[] ip) {
		try {
			System.out.println("客户端传来的ip:"+new String(ip));
			return "success"+new String(ip);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return "error";
	}
}

IpManageSvr的作用是发布这个webservice服务,只有你的服务发布了并且一直处于运行状态,别人才可以调用你的webservice服务,代码如下:

package cn.zhao.server;

import javax.xml.ws.Endpoint;

import cn.zhao.svc.impl.IpManageImpl;

public class IpManageSvr {
	public static void main(String[] args) {
		//本webservice的地址
		String address="http://localhost:8888/ceshi_webservice_server/svc01";
		Endpoint endpoint = Endpoint.publish(address, new IpManageImpl());
		System.out.println(endpoint);
		System.out.println("通过jdk发布webservice成功");
	}
}

address是你指定的你这个webservice服务的访问地址,如果其他外部想调用你的这个webservice服务,你只需要提供这个地址给别人就行了。我们直接运行这个main函数,结果如下:

image.png

好了,经过上面两步,webservice的服务提供方的工作就做完了。

webservice调用方

一、在服务提供方提供了地址(比如上面的http://localhost:8888/ceshi_webservice_server/svc01)之后,客户端可以通过http://localhost:8888/ceshi_webservice_server/svc01?wsdl的方式来查看服务提供方提供的接口调用格式:

image.png

webservice本质上就是发送和接受的soap消息,soap消息就是html+xml片段,这样这里的soap消息跟现在吹牛逼经常使用soa并没有关系。解释下wsdl(webservice defination  language)的结构:

  types:定义消息

  message:引用消息

  porttype:定义sei(service  endpoint  interface)

  binding:sei的实现类          

  service:sei的容器

看接口的结构要访问types里面定义的import进来的文件:

image.png

二、知道了webservice服务的wsdl我们之后需要做的就是利用jdk提供的工具自动生成代码,我们再在这个代码的基础上去编写代码调用就行了,怎么自动生成代码呢?wsimport -keep http://localhost:8888/ceshi_webservice_server/svc01?wsdl或者wsimport -keep e:\\xx.wsdl,其中xx.wsdl是你把http://localhost:8888/ceshi_webservice_server/svc01?wsdl的内容拷贝到了本地之后的文件,两种方式都行,其中wsimport是安装jdk时bin目录中自带的工具,我们新建一个工程名字为ceshi_webservice_client,我们使用第一种:

image.png

执行完之后,eclipse中ceshi_webservice_client项目的结构变成了下面的样子:

image.png

红框中的类都是生成的,那么我们就来编写个测试类测试一下吧。

三、调用webservice服务,编写IpManageClient.java:

package cn.zhao.client;

import cn.zhao.svc.impl.IpManageImpl;
import cn.zhao.svc.impl.IpManageImplService;

public class IpManageClient {
	public static void main(String[] args) {
		//factory可以得到sei的实现类对象
		IpManageImplService factory=new IpManageImplService();
		//接口 
		IpManageImpl ipManageImpl = factory.getIpManageImplPort();
		System.out.println(ipManageImpl.getClass());//ipManageImpl是动态产生的代理对象
		
		String ret= ipManageImpl.auth("192.168.1.1".getBytes());
		System.out.println("服务端返回的值:"+ret);
	}
}

运行结果:

image.png

四、完成

三、一步步使用cxf实现webservice

前边详细说了如何“一步步使用jdk实现webservice”,今天我们来看看如何使用比较流行的cxf来实现webservice。既然jdk可以实现webservice,为什么还要cxf呢?cxf支持list map set Student等所有类型,jdk的ws不支持map,另外针对webservice服务,jdk是没有拦截器的,cxf才有。

我们这里基于“一步步使用jdk实现webservice”项目改造,服务端和客户端都是用cxf来实现。

服务端

服务端不需要改变任何代码,要是什么jar包都不加就是使用的jdk发布webservice,假如加入了cxf的jar包那么就是使用的cxf了。因此我们要做的就是在服务端加入cxf的jar包,cxf需要的jar包都在cxf的压缩包里吗了,上面已经提供。

这里我们加入jar包之后,直接运行IpManageSvr类:

package cn.zhao.server;

import javax.xml.ws.Endpoint;

import cn.zhao.svc.impl.IpManageImpl;

public class IpManageSvr {
	public static void main(String[] args) {
		//本webservice的地址
		String address="http://localhost:8888/ceshi_webservice_server/svc01";
		Endpoint endpoint = Endpoint.publish(address, new IpManageImpl());
		System.out.println(endpoint);
		System.out.println("通过cxf发布webservice成功");
	}
}

我只时把通过jdk改为了通过cxf。

运行结果如下:

image.png

就这样,不要改变任何代码,你就通过cxf把webservice服务发布成功了。

看下wsdl文档:

image.png

比jdk的简洁了很多。

webservice调用端

一、新建ceshi_webservice_client java项目,重新生成代码,类似jdk的wsimport,这次试用的命令是wsdl2java  http://localhost:8888/ceshi_webservice_server/svc01?wsdl 或者wsdl2java   e:\\xx.wsdl。

image.png

查看代码结构:

image.png

我们看到,跟jdk生成webservice时生成的包结构也有稍微的差异。

二、新建个客户端调用类

package cn.zhao.client;

import cn.zhao.svc.IpManage;
import cn.zhao.svc.impl.IpManageImplService;

public class IpManageClient {
	public static void main(String[] args) {
		//factory可以得到sei的实现类对象
		IpManageImplService factory=new IpManageImplService();
		//接口 
		IpManage ipManage = factory.getIpManageImplPort();
		System.out.println(ipManage.getClass());//ipManageImpl是动态产生的代理对象
		
		String ret= ipManage.auth("192.168.1.1".getBytes());
		System.out.println("服务端返回的值:"+ret);
	}
}

三、加入cxf的jar包,最终的项目结构如下:

image.png

四、运行:

客户端结果f服务端结果
image.pngimage.png

四、cxf开发webservice加入权限验证拦截器与日志拦截器

cxf里面的日志拦截器可以打印soap消息信息,有出拦截器和入拦截器之分。当然这些soap消息也可以通过工具来看到。如何加入日志拦截器呢,我们还是接着“一步步使用cxf实现webservice”里面的代码。

服务端

修改IpManageSvr发布类:

package cn.zhao.server;

import java.util.List;

import javax.xml.ws.Endpoint;

import org.apache.cxf.interceptor.Interceptor;
import org.apache.cxf.interceptor.LoggingInInterceptor;
import org.apache.cxf.interceptor.LoggingOutInterceptor;
import org.apache.cxf.jaxws.EndpointImpl;
import org.apache.cxf.message.Message;

import cn.zhao.svc.impl.IpManageImpl;

public class IpManageSvr {
	public static void main(String[] args) {
		//本webservice的地址
		String address="http://localhost:8888/ceshi_webservice_server/svc01";
		Endpoint endpoint = Endpoint.publish(address, new IpManageImpl());
		//加入日志拦截器
		EndpointImpl eImpl=(EndpointImpl)endpoint;
		List<Interceptor<? extends Message>> inInterceptors=eImpl.getInInterceptors();
		//加入日志入拦截器
		inInterceptors.add(new LoggingInInterceptor());
		
		//加入日志出拦截器
		List<Interceptor<? extends Message>> outInterceptors = eImpl.getOutInterceptors();
		outInterceptors.add(new LoggingOutInterceptor());
		System.out.println(endpoint);
		System.out.println("通过cxf发布webservice成功");
	}
}

客户端

修改IpManageClient调用类:

package cn.zhao.client;

import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.interceptor.LoggingInInterceptor;
import org.apache.cxf.interceptor.LoggingOutInterceptor;

import cn.zhao.svc.IpManage;
import cn.zhao.svc.impl.IpManageImplService;

public class IpManageClient {
	public static void main(String[] args) {
		//factory可以得到sei的实现类对象
		IpManageImplService factory=new IpManageImplService();
		//接口 
		IpManage ipManage = factory.getIpManageImplPort();
		System.out.println(ipManage.getClass());//ipManageImpl是动态产生的代理对象
		
		//在方法调用之前加入日志出入拦截器
		//调用端的出对应服务端的入;调用端的入对应服务端的出;
		Client client = ClientProxy.getClient(ipManage);
		client.getOutInterceptors().add(new LoggingOutInterceptor());
		client.getInInterceptors().add(new LoggingInInterceptor());
		
		
		String ret= ipManage.auth("192.168.1.1".getBytes());
		System.out.println("服务端返回的值:"+ret);
	}
}

分别运行服务和客户端,结果如下:

服务端打印信息结果:

信息: started o.e.j.s.h.ContextHandler{/ceshi_webservice_server,null}

org.apache.cxf.jaxws22.EndpointImpl@36905f7a

通过cxf发布webservice成功

八月 18, 2018 2:16:02 下午 org.apache.cxf.services.IpManageImplService.IpManageImplPort.IpManage

信息: Inbound Message

----------------------------

ID: 1

Address: http://localhost:8888/ceshi_webservice_server/svc01?wsdl

Encoding: UTF-8

Http-Method: GET

Content-Type: text/xml

Headers: {Accept=[*/*], Cache-Control=[no-cache], connection=[keep-alive], content-type=[text/xml], Host=[localhost:8888], Pragma=[no-cache], User-Agent=[Apache CXF 2.5.9]}

--------------------------------------

八月 18, 2018 2:16:02 下午 org.apache.cxf.services.IpManageImplService.IpManageImplPort.IpManage

信息: Inbound Message

----------------------------

ID: 2

Address: http://localhost:8888/ceshi_webservice_server/svc01?wsdl=IpManage.wsdl

Encoding: UTF-8

Http-Method: GET

Content-Type: text/xml

Headers: {Accept=[*/*], Cache-Control=[no-cache], connection=[keep-alive], content-type=[text/xml], Host=[localhost:8888], Pragma=[no-cache], User-Agent=[Apache CXF 2.5.9]}

--------------------------------------

八月 18, 2018 2:16:06 下午 org.apache.cxf.services.IpManageImplService.IpManageImplPort.IpManage

信息: Inbound Message

----------------------------

ID: 3

Address: http://localhost:8888/ceshi_webservice_server/svc01

Encoding: UTF-8

Http-Method: POST

Content-Type: text/xml; charset=UTF-8

Headers: {Accept=[*/*], Cache-Control=[no-cache], connection=[keep-alive], Content-Length=[191], content-type=[text/xml; charset=UTF-8], Host=[localhost:8888], Pragma=[no-cache], SOAPAction=[""], User-Agent=[Apache CXF 2.5.9]}

Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:auth xmlns:ns2="http://svc.zhao.cn/"><arg0>MTkyLjE2OC4xLjE=</arg0></ns2:auth></soap:Body></soap:Envelope>

--------------------------------------

客户端传来的ip:192.168.1.1

八月 18, 2018 2:16:06 下午 org.apache.cxf.services.IpManageImplService.IpManageImplPort.IpManage

信息: Outbound Message

---------------------------

ID: 3

Encoding: UTF-8

Content-Type: text/xml

Headers: {}

Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:authResponse xmlns:ns2="http://svc.zhao.cn/"><return>success192.168.1.1</return></ns2:authResponse></soap:Body></soap:Envelope>

--------------------------------------

客户端打印信息:

八月 18, 2018 2:16:03 下午 org.apache.cxf.service.factory.ReflectionServiceFactoryBean buildServiceFromWSDL

信息: Creating Service {http://impl.svc.zhao.cn/}IpManageImplService from WSDL: http://localhost:8888/ceshi_webservice_server/svc01?wsdl

class com.sun.proxy.$Proxy25

八月 18, 2018 2:16:06 下午 org.apache.cxf.services.IpManageImplService.IpManageImplPort.IpManage

信息: Outbound Message

---------------------------

ID: 1

Address: http://localhost:8888/ceshi_webservice_server/svc01

Encoding: UTF-8

Content-Type: text/xml

Headers: {Accept=[*/*], SOAPAction=[""]}

Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:auth xmlns:ns2="http://svc.zhao.cn/"><arg0>MTkyLjE2OC4xLjE=</arg0></ns2:auth></soap:Body></soap:Envelope>

--------------------------------------

八月 18, 2018 2:16:07 下午 org.apache.cxf.services.IpManageImplService.IpManageImplPort.IpManage

信息: Inbound Message

----------------------------

ID: 1

Response-Code: 200

Encoding: UTF-8

Content-Type: text/xml;charset=UTF-8

Headers: {Content-Length=[213], content-type=[text/xml;charset=UTF-8], Server=[Jetty(7.5.4.v20111024)]}

Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:authResponse xmlns:ns2="http://svc.zhao.cn/"><return>success192.168.1.1</return></ns2:authResponse></soap:Body></soap:Envelope>

--------------------------------------

服务端返回的值:success192.168.1.1

cxf自带的日志拦截器主要用于打印webservice调用的时候传递的参数以及返回的参数。如果我想自定义拦截器怎么办呢?比如我想定义一个拦截器,指定调用我这个服务需要输入用户名密码,该怎么实现呢?

一、服务端在入拦截器list中加入获取权限信息的拦截器:

image.png

其中,AuthInterc的代码如下:

package cn.zhao.server;

import javax.xml.namespace.QName;

import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.headers.Header;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
/**
 * 定义cxf权限拦截器
 */
public class AuthInterc extends AbstractPhaseInterceptor<SoapMessage>{
	public AuthInterc() {
		//webservice有很多阶段,这里指定在哪个阶段使用这个拦截器
		super(Phase.PRE_PROTOCOL);
	}

	/**
	 * <soap:Envelope>
	 * <head>
	 *    <userinfo>
	 *       <name>roadjava></name>
	 *       <pwd>123</pwd>
	 *    </userinfo>
	 * </head>
	 * <soap:Body>
	 *    <ns2:auth>
	 *       <arg0>MTkyLjE2OA==</arg0>
	 *    </ns2:auth>
	 * </soap:Body>
	 * </soap:Envelope>
	 */
	@Override
	public void handleMessage(SoapMessage message) throws Fault {
		//获取soap消息头部,从头部获取userinfo标签,userinfo标签是客户端调用时在
		//出拦截器列表中加入的
		Header header = message.getHeader(new QName("userinfo"));
		if(header!=null){
		    Element element=(Element) header.getObject();
		    NodeList nameList = element.getElementsByTagName("name");
		    NodeList pwdList = element.getElementsByTagName("pwd");
		     String name=nameList.item(0).getFirstChild().getNodeValue();
			 String pwd=pwdList.item(0).getFirstChild().getNodeValue();
			 if ("roadjava".equals(name)&&"123".equals(pwd)) {
				//不拦截,执行调用
			}else {
				throw new Fault(new RuntimeException("用户名或密码不正确"));
			}
		}else {
			throw new Fault(new RuntimeException("用户信息不存在"));
		}
	}
}

二、客户端调用webservice的时候在出拦截器list中加入权限验证拦截器

image.png

其中AuthIntercClient类代码:

package cn.zhao.client;

import java.util.List;

import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.headers.Header;
import org.apache.cxf.helpers.DOMUtils;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;
import org.apache.xml.utils.DOMHelper;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/**
 * 自定义拦截器
 * @author zhao
 *
 */
public class AuthIntercClient extends AbstractPhaseInterceptor<SoapMessage>{
	private String name;
	private String pwd;
	public AuthIntercClient(String name,String pwd) {
		super(Phase.PRE_PROTOCOL);
		this.name=name;
		this.pwd=pwd;
	}

	/**
	 * <soap:Envelope>
	 * <head>
	 *     <userinfo>
	 * 		<name>roadjava</name>
	 * 		<pwd>123</pwd>
	 * 	   </userinfo>
	 * </head>
	 * <soap:Body>
	 * 		<ns2:auth>
	 * 			<arg0>MTkyLjE2OA==</arg0>
	 * 		</ns2:auth>
	 * </soap:Body>
	 * </soap:Envelope>
	 */
	@Override
	public void handleMessage(SoapMessage message) throws Fault {
		List<Header> headers = message.getHeaders();
		//Document createDocument = DOMUtils.createDocument();
		//采用java自带的dom生成器
		DocumentBuilderFactory dbf=DocumentBuilderFactory.newInstance();
		DocumentBuilder db=null;
		try {
			db = dbf.newDocumentBuilder();
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		}
		//创建userinfo标签
		Document document = db.newDocument();
		Element userinfoEl = document.createElement("userinfo");
		Element name = document.createElement("name");
		Element pwd = document.createElement("pwd");
		name.setTextContent(this.name);
		pwd.setTextContent(this.pwd);
		userinfoEl.appendChild(name);
		userinfoEl.appendChild(pwd);
		document.appendChild(userinfoEl);
		//创建header标签并加入到headers这个list中
		headers.add(new Header(new QName("userinfo"), userinfoEl));
	}
}

三、运行服务端和客户端

客户端打印信息:

信息: Outbound Message

---------------------------

ID: 1

Address: http://localhost:8888/ceshi_webservice_server/svc01

Encoding: UTF-8

Content-Type: text/xml

Headers: {Accept=[*/*], SOAPAction=[""]}

Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Header><userinfo><name>roadjava</name><pwd>123</pwd></userinfo></soap:Header><soap:Body><ns2:auth xmlns:ns2="http://svc.zhao.cn/"><arg0>MTkyLjE2OC4xLjE=</arg0></ns2:auth></soap:Body></soap:Envelope>

--------------------------------------

八月 18, 2018 2:40:43 下午 org.apache.cxf.services.IpManageImplService.IpManageImplPort.IpManage

信息: Inbound Message

----------------------------

ID: 1

Response-Code: 200

Encoding: UTF-8

Content-Type: text/xml;charset=UTF-8

Headers: {Content-Length=[213], content-type=[text/xml;charset=UTF-8], Server=[Jetty(7.5.4.v20111024)]}

Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:authResponse xmlns:ns2="http://svc.zhao.cn/"><return>success192.168.1.1</return></ns2:authResponse></soap:Body></soap:Envelope>

--------------------------------------

服务端返回的值:success192.168.1.1

服务端打印信息:

信息: Inbound Message

----------------------------

ID: 3

Address: http://localhost:8888/ceshi_webservice_server/svc01

Encoding: UTF-8

Http-Method: POST

Content-Type: text/xml; charset=UTF-8

Headers: {Accept=[*/*], Cache-Control=[no-cache], connection=[keep-alive], Content-Length=[274], content-type=[text/xml; charset=UTF-8], Host=[localhost:8888], Pragma=[no-cache], SOAPAction=[""], User-Agent=[Apache CXF 2.5.9]}

Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Header><userinfo><name>roadjava</name><pwd>123</pwd></userinfo></soap:Header><soap:Body><ns2:auth xmlns:ns2="http://svc.zhao.cn/"><arg0>MTkyLjE2OC4xLjE=</arg0></ns2:auth></soap:Body></soap:Envelope>

--------------------------------------

客户端传来的ip:192.168.1.1

八月 18, 2018 2:40:43 下午 org.apache.cxf.services.IpManageImplService.IpManageImplPort.IpManage

信息: Outbound Message

---------------------------

ID: 3

Encoding: UTF-8

Content-Type: text/xml

Headers: {}

Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:authResponse xmlns:ns2="http://svc.zhao.cn/"><return>success192.168.1.1</return></ns2:authResponse></soap:Body></soap:Envelope>

--------------------------------------

这是没有任何问题的。

四、如果我在客户端调用的时候传入错误的用户名和密码呢?当然会调用不成功了,验证如下:

client.getOutInterceptors().add(new AuthIntercClient("xxx", "123"));

客户端打印信息:

信息: Outbound Message

---------------------------

ID: 1

Address: http://localhost:8888/ceshi_webservice_server/svc01

Encoding: UTF-8

Content-Type: text/xml

Headers: {Accept=[*/*], SOAPAction=[""]}

Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Header><userinfo><name>xxx</name><pwd>123</pwd></userinfo></soap:Header><soap:Body><ns2:auth xmlns:ns2="http://svc.zhao.cn/"><arg0>MTkyLjE2OC4xLjE=</arg0></ns2:auth></soap:Body></soap:Envelope>

--------------------------------------

八月 18, 2018 2:44:53 下午 org.apache.cxf.services.IpManageImplService.IpManageImplPort.IpManage

信息: Inbound Message

----------------------------

ID: 1

Response-Code: 500

Encoding: UTF-8

Content-Type: text/xml;charset=UTF-8

Headers: {Content-Length=[222], content-type=[text/xml;charset=UTF-8], Server=[Jetty(7.5.4.v20111024)]}

Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><soap:Fault><faultcode>soap:Server</faultcode><faultstring>用户名或密码不正确</faultstring></soap:Fault></soap:Body></soap:Envelope>

--------------------------------------

Exception in thread "main" javax.xml.ws.soap.SOAPFaultException: 用户名或密码不正确

at org.apache.cxf.jaxws.JaxWsClientProxy.invoke(JaxWsClientProxy.java:156)

at com.sun.proxy.$Proxy25.auth(Unknown Source)

at cn.zhao.client.IpManageClient.main(IpManageClient.java:27)

Caused by: org.apache.cxf.binding.soap.SoapFault: 用户名或密码不正确

at org.apache.cxf.binding.soap.interceptor.Soap11FaultInInterceptor.unmarshalFault(Soap11FaultInInterceptor.java:75)

at org.apache.cxf.binding.soap.interceptor.Soap11FaultInInterceptor.handleMessage(Soap11FaultInInterceptor.java:46)

at org.apache.cxf.binding.soap.interceptor.Soap11FaultInInterceptor.handleMessage(Soap11FaultInInterceptor.java:35)

at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:263)

at org.apache.cxf.interceptor.AbstractFaultChainInitiatorObserver.onMessage(AbstractFaultChainInitiatorObserver.java:114)

at org.apache.cxf.binding.soap.interceptor.CheckFaultInterceptor.handleMessage(CheckFaultInterceptor.java:69)

at org.apache.cxf.binding.soap.interceptor.CheckFaultInterceptor.handleMessage(CheckFaultInterceptor.java:34)

at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:263)

at org.apache.cxf.endpoint.ClientImpl.onMessage(ClientImpl.java:801)

at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.handleResponseInternal(HTTPConduit.java:1679)

at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.handleResponse(HTTPConduit.java:1517)

at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.close(HTTPConduit.java:1425)

at org.apache.cxf.io.CacheAndWriteOutputStream.postClose(CacheAndWriteOutputStream.java:50)

at org.apache.cxf.io.CachedOutputStream.close(CachedOutputStream.java:188)

at org.apache.cxf.transport.AbstractConduit.close(AbstractConduit.java:56)

at org.apache.cxf.transport.http.HTTPConduit.close(HTTPConduit.java:650)

at org.apache.cxf.interceptor.MessageSenderInterceptor$MessageSenderEndingInterceptor.handleMessage(MessageSenderInterceptor.java:62)

at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:263)

at org.apache.cxf.endpoint.ClientImpl.doInvoke(ClientImpl.java:531)

at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:462)

at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:365)

at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:318)

at org.apache.cxf.frontend.ClientProxy.invokeSync(ClientProxy.java:95)

at org.apache.cxf.jaxws.JaxWsClientProxy.invoke(JaxWsClientProxy.java:134)

... 2 more

五、cxf与spring整合在web项目中的应用

webservice服务端

一、新建动态web工程ws02server,加入cxfjar包,配置web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" 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">
	<display-name>ws02server</display-name>
 <!-- 设置Spring容器加载配置文件路径 -->
 <context-param>
     <param-name>contextConfigLocation</param-name>
     <param-value>classpath:beans.xml</param-value>
 </context-param>
	<!-- 加载Spring容器配置 -->
 <listener>
     <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
 </listener>
  <!-- 配置cxf的servlet -->
 <servlet>
     <servlet-name>CXFService</servlet-name>
     <servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
 </servlet>
  
 <servlet-mapping>
     <servlet-name>CXFService</servlet-name>
     <url-pattern>/*</url-pattern>
 </servlet-mapping>
	
</web-app>

新建接口LogisticsService:

package cn.zhao.spring.logistics;

import javax.jws.WebMethod;
import javax.jws.WebService;

/**
 * 物流信息接口
 * @author zhao
 *
 */
@WebService
public interface LogisticsService {
	@WebMethod
	public LogisticsInfo getByOrderNo(Integer id);
}

新建LogisticsService接口的实现类:

package cn.zhao.spring.logistics;

import javax.jws.WebService;

/**
 * 接口实现
 * @author zhao
 *
 */
@WebService
public class LogisticsServiceImp implements LogisticsService{
	public LogisticsInfo getByOrderNo(Integer id){
		System.out.println("传来的订单id号:"+id);
		LogisticsInfo logisticsInfo = new LogisticsInfo(1,id,"八千家物流");
		System.out.println(logisticsInfo);
		return logisticsInfo;
	}
}

返回的实体类LogisticsInfo的信息:

package cn.zhao.spring.logistics;
/**
 * 物流信息
 * @author zhao
 *
 */
public class LogisticsInfo {
	//主键
	private Integer id;
	//该物流信息关联的订单号
	private Integer orderNo;
	//物流公司名称
	private String name;
	public Integer getId() {
		return id;
	}
	public void setId(Integer id) {
		this.id = id;
	}
	public Integer getOrderNo() {
		return orderNo;
	}
	public void setOrderNo(Integer orderNo) {
		this.orderNo = orderNo;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	@Override
	public String toString() {
		return "LogisticsInfo [id=" + id + ", orderNo=" + orderNo + ", name="
				+ name + "]";
	}
	public LogisticsInfo(Integer id, Integer orderNo, String name) {
		super();
		this.id = id;
		this.orderNo = orderNo;
		this.name = name;
	}
	public LogisticsInfo(){
		
	}
}

调用权限拦截器代码:

package cn.zhao.spring.logistics;

import javax.xml.namespace.QName;

import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.headers.Header;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

public class CheckUserinfo extends AbstractPhaseInterceptor<SoapMessage>{
	public CheckUserinfo() {
		super(Phase.PRE_PROTOCOL);
	}

	/**
	 * <soap:Envelope>
	 * <head>
	 * <userinfo>
	 * <name>roadjava</name>
	 * <pwd>123</pwd>
	 * </userinfo>
	 * </head>
	 * <soap:Body>
	 * <ns2:auth>
	 * <arg0>MTkyLjE2OA==</arg0>
	 * </ns2:auth>
	 * </soap:Body>
	 * </soap:Envelope>
	 */
	@Override
	public void handleMessage(SoapMessage message) throws Fault {
		Header header = message.getHeader(new QName("userinfo"));
		if(header!=null){
		    Element element=(Element) header.getObject();
		    NodeList nameList = element.getElementsByTagName("name");
		    NodeList pwdList = element.getElementsByTagName("pwd");
		     String name=nameList.item(0).getFirstChild().getNodeValue();
			 String pwd=pwdList.item(0).getFirstChild().getNodeValue();
			 if ("roadjava".equals(name)&&"123".equals(pwd)) {
				
			}else {
				throw new Fault(new RuntimeException("用户名或密码不正确"));
			}
		}else {
			
			throw new Fault(new RuntimeException("没有信息"));
		}
	}
}

在spring的配置文件beans.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"
	xmlns:jaxws="http://cxf.apache.org/jaxws"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
           http://cxf.apache.org/jaxws 
           http://cxf.apache.org/schemas/jaxws.xsd">
     <!-- 不用再src下建立文件夹了,这些文件都是cxf jar包里的文件 -->      
	<import resource="classpath:META-INF/cxf/cxf.xml"/>
	<import resource="classpath:META-INF/cxf/cxf-extension-soap.xml"/>
	<import resource="classpath:META-INF/cxf/cxf-servlet.xml"/>
    <!-- 终端的配置 ,暴露webservice,直接写类没法注入其他依赖的对象,要单独一个bean,
        本身就是一个web工程,故address不在需要加那些http
        -->   
      <bean id="logisticsServiceImpl" class="cn.zhao.spring.logistics.LogisticsServiceImp"></bean>  
      <!-- #代表使用spring容器中的类 ,address="/logistics"表示将来暴露的webservice的地址为:
      http://localhost:8080/ws02server/logistics-->
     <jaxws:endpoint address="/logistics"  implementor="#logisticsServiceImpl">
     	<jaxws:inInterceptors >
     		<bean class="cn.zhao.spring.logistics.CheckUserinfo"></bean>
     	</jaxws:inInterceptors>
     </jaxws:endpoint>   
</beans>

二、服务端项目结构:

image.png

三、启动服务端,并访问http://localhost:8080/ws02server/,因为配置了org.apache.cxf.transport.servlet.CXFServlet拦截所有的地址,所以这里你访问首页就可以看到本项目提供的webservice服务列表。

image.png

webservice客户端

一、新建ws02client web工程,导入cxfjar包以及其他使用的jar包(spring,struts2等)

二、配置web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" 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">
	<display-name>ws02client</display-name>
 <!-- 设置Spring容器加载配置文件路径 -->
 <context-param>
     <param-name>contextConfigLocation</param-name>
     <param-value>classpath:beans.xml</param-value>
 </context-param>
	<!-- 加载Spring容器配置 -->
 <listener>
     <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
 </listener>
  <!-- 只使用别人的webservice就不用配置cxf的servlet了 -->
  <filter>
		<filter-name>struts2</filter-name>
		<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>struts2</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
	
</web-app>

三、使用wsdl2java 自动生成客户端的类,执行命令wsdl2java http://localhost:8080/ws02server/logistics?wsdl:

image.png

生成之后项目结构如下所示:

image.png

四、配置struts2.xml:

<?xml version="1.0" encoding="utf-8"?>
 <!DOCTYPE struts PUBLIC
	"-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
	"http://struts.apache.org/dtds/struts-2.3.dtd">
<struts>
     <!-- struts2的实例化由web容器交由spring -->
	<constant name="struts.objectFactory" value="spring" />
	<package extends="struts-default" name="ws02client" namespace="/">
		<action name="listLogistics"  class="listLogistics" >
			<result name="listLogisticsInfo">/listLogisticsInfo.jsp</result>
		</action>
	</package>
</struts>

五、struts2的action:

package cn.spring.controller;

import javax.annotation.Resource;

import org.apache.struts2.dispatcher.RequestMap;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;

import cn.zhao.spring.logistics.LogisticsInfo;
import cn.zhao.spring.logistics.LogisticsService;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionSupport;

/**
 * 控制器依赖的不再是自己的service,而是第三方的ws,直接到别人的数据库中去取了
 */
@Controller @Scope("prototype")
public class ListLogistics extends ActionSupport {
	@Resource
	private LogisticsService logisticsService;
	private LogisticsInfo logisticsInfo;
	
	public LogisticsInfo getLogisticsInfo() {
		return logisticsInfo;
	}

	public void setLogisticsInfo(LogisticsInfo logisticsInfo) {
		this.logisticsInfo = logisticsInfo;
	}

	@Override
	public String execute() throws Exception {
		logisticsInfo= logisticsService.getByOrderNo(22);
		RequestMap object = (RequestMap) ActionContext.getContext().get("request");
		object.put("logisticsInfo", logisticsInfo);
		return "listLogisticsInfo";
	}
	
}

六、beans.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"
	xmlns:jaxws="http://cxf.apache.org/jaxws"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
           http://cxf.apache.org/jaxws 
           http://cxf.apache.org/schemas/jaxws.xsd
             http://www.springframework.org/schema/context 
             http://www.springframework.org/schema/context/spring-context-2.5.xsd
           ">
	<!-- 扫描 -->
	<context:component-scan base-package="cn" />
     <!-- 客户端的配置 serviceClass配置成接口-->   
     <jaxws:client id= "logisticsService" serviceClass="cn.zhao.spring.logistics.LogisticsService"
      address="http://localhost:8080/ws02server/logistics">
     <jaxws:outInterceptors>
       <bean class="cn.spring.client.test.AddUserInfo" id="addUserInfo"> 
       	<constructor-arg index="0" value="roadjava"/>
       	<constructor-arg  index="1" value="123"/>
       </bean>
     </jaxws:outInterceptors>
     </jaxws:client>
</beans>

七、权限类:

package cn.spring.client.test;

import java.util.List;

import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.headers.Header;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

public class AddUserInfo extends AbstractPhaseInterceptor<SoapMessage>{
	private String name;
	private String pwd;
	public AddUserInfo(String name,String pwd) {
		super(Phase.PRE_PROTOCOL);
		this.name=name;
		this.pwd=pwd;
	}

	/**
	 * <soap:Envelope>
	 * <head>
	 * <userinfo>
	 * <name>roadjava</name>
	 * <pwd>123</pwd>
	 * </userinfo>
	 * </head>
	 * <soap:Body>
	 * <ns2:auth>
	 * <arg0>MTkyLjE2OA==</arg0>
	 * </ns2:auth>
	 * </soap:Body>
	 * </soap:Envelope>
	 */
	@Override
	public void handleMessage(SoapMessage message) throws Fault {
		List<Header> headers = message.getHeaders();
		DocumentBuilderFactory dbf=DocumentBuilderFactory.newInstance();
		DocumentBuilder db=null;
		try {
			db = dbf.newDocumentBuilder();
		} catch (ParserConfigurationException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		Document document = db.newDocument();
		Element userinfoEl = document.createElement("userinfo");
		Element name = document.createElement("name");
		Element pwd = document.createElement("pwd");
		name.setTextContent(this.name);
		pwd.setTextContent(this.pwd);
		userinfoEl.appendChild(name);
		userinfoEl.appendChild(pwd);
		document.appendChild(userinfoEl);
		headers.add(new Header(new QName("userinfo"), userinfoEl));
	}

	
}

八、listLogisticsInfo.jsp文件:

<%@ page language="java" contentType="text/html; charset=utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>显示物流信息</title>
</head>
<body>
 ${logisticsInfo.id }----
 ${logisticsInfo.name }----
 ${logisticsInfo.orderNo }----
</body>
</html>

 

九、写好webservice的客户端之后,把客户端也启动,访问客户端的action:http://localhost:8081/ws02client/listLogistics,徐国struts2的同学都知道,会默认调用ListLogistics类里面的execute方法,运行结果:

image.png

服务端也打印了相关信息:

image.png

好,这样就完成了cxf与spring整合在web项目中的应用。

六、使用eclipse自带的webservice explorer来调用webservice

一、使用eclipse自带的webservice explorer来调用webservice,可以在写客户端调用代码之前测试webservice

image.png

二、输入后点击go:

image.png

三、这里的operations区域展示的就是提供的可供调用的方法,点击方法名称即可调用

image.png

如果要加入参数,可以直接点击add,加入参数,如果需要加入header,可以切到source模式,输入要加入的header:

image.png

四、运行返回结果:

image.png

七、axis2实现webservice

服务端

一、新建web工程d3(名字叫什么没有关系啦),导入axis2所需jar包,我这里使用的是axis2 1.7.8的版本,配置web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" 
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
  <display-name>d3</display-name><!-- 项目名,创建项目时候自动生成的 -->
    <servlet>  
        <servlet-name>AxisServlet</servlet-name>  
        <servlet-class>org.apache.axis2.transport.http.AxisServlet</servlet-class>  
        <load-on-startup>1</load-on-startup>  
    </servlet>  
    <servlet-mapping>  
        <servlet-name>AxisServlet</servlet-name>  
        <!-- 原先拦截的是/apis/*,按照最简默认配置,这里要写为/services/*,wsdl中生成的会默认使用services,如果配置成apis,
        将会拦截不到:
        <http:address location="http://localhost:8081/d3/services/companySvc.companySvcHttpEndpoint/"/>
         -->
        <url-pattern>/services/*</url-pattern>  
    </servlet-mapping>  
</web-app>

注意:如注释中所说,这里拦截url要配置成/services/*,不能随便写。

二、新建接口,用来提供webservice服务

package com.roadjava.apis;

public interface CompanySvc {
	String companyScore(String name);
}

三、新建上面这个接口的实现类

package com.roadjava.apis.impl;

import com.roadjava.apis.CompanySvc;

public class CompanySvcImpl implements CompanySvc{

	@Override
	public String companyScore(String name) {
		return "企业"+name+"综合评分为3分(满10分)";
	}

}

作用很简单,就是传入一个公司名称,返回这个公司的综合评分,这是模拟的业务。

四、在WEB-INF/services/config/META-INF文件夹下面新建一个services.xml文件,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<service name="companySvc">  <!-- 指定服务名,随便定义 -->
    <description>测试axis2实现webservice</description><!-- 服务的作用说明,可写可不写 -->
     <!-- 指定要发布的类路径 -->
    <parameter name="ServiceClass">com.roadjava.apis.impl.CompanySvcImpl</parameter>  
   <!-- 类里面的方法,有其他方法就在写个operation标签 -->
    <operation name="companyScore">  <!-- 类里面的方法名 -->
        <messageReceiver class="org.apache.axis2.rpc.receivers.RPCMessageReceiver" />  
    </operation>   
</service>

注意:上面的路径除了config这个名字是自己起的,其他路径名字都是固定的,不能修改。项目结构:

image.png

五、运行项目,那么按照上面的配置,访问http://localhost:8081/d3/services/companySvc?wsdl即可看到webservice的wsdl文件,其中companySvc是上面services.xml中的service的name值。

image.png

客户端

一、新建axis2client项目,导入axis2所需jar包,我这里使用的是axis2 1.7.8的版本,新建一个类axis2client.controller.CompanyController,用来对webservice服务发起请求:

package axis2client.controller;

import javax.xml.namespace.QName;

import org.apache.axis2.AxisFault;
import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.client.Options;
import org.apache.axis2.rpc.client.RPCServiceClient;

public class CompanyController {
public static void main(String[] args) {
	 try {
         //本机tomcat端口默认为8081,参数是wsdl网址的一部分
         EndpointReference eRef = new EndpointReference("http://localhost:8081/d3/services/companySvc");  
         RPCServiceClient sender = new RPCServiceClient();  
         Options options = sender.getOptions();  
         options.setTimeOutInMilliSeconds(2*20000L);//超时时间20s  
         options.setTo(eRef);  
         /**
          * 参数:
          * 1:在网页上执行http://localhost:8081/d3/apis/companySvc?wsdl后
          * types--->schema标签的targetNamespace="http://impl.apis.roadjava.com">
          * 2:types--->schema-->element的name="companyScore",即接口名
          */
         QName qname = new QName("http://impl.apis.roadjava.com", "companyScore"); 
         String str = "阿里巴巴";  //方法的入参
         Object[] param = new Object[]{str};  
         Class<?>[] types = new Class[]{String.class};  //这是针对返回值类型的  
         /** 
          * RPCServiceClient类的invokeBlocking方法调用了WebService中的方法。 
          * invokeBlocking方法有三个参数 
          * 第一个参数的类型是QName对象,表示要调用的方法名; 
          * 第二个参数表示要调用的WebService方法的参数值,参数类型为Object[]; 
          * 第三个参数表示WebService方法的返回值类型的Class对象,参数类型为Class[]。 
          * 当方法没有参数时,invokeBlocking方法的第二个参数值不能是null,而要使用new Object[]{}。 
          */  
         Object[] response1 = sender.invokeBlocking(qname, param, types);  
         System.out.println(response1[0]);
     } catch (AxisFault e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
     }
}
}

二、运行结果:

image.png

三、客户端项目代码结构:

image.png

好了,这样就完成了axis2发布webservice以及客户端的调用过程。

总结

①、axis2跟cxf还有jdk实现webservice的方式有所不同,axis2不需要通过wsdl2java命令或者wsimport命令进行类的自动生成。

②、axis2有自己的配置文件,而且默认有自己的固定路径,cxf还有jdk实现webservice是不需要自己的配置文件的。

③、axis2不依赖@WebService、@WebMethod这些注解

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值