如今REST风格的服务比较流行,调用起来也方便,受到广泛欢迎,我在网上找了一堆材料来试图搭建,过程中碰到很多问题,我没有找到使用Spring5.x来搭建的博客,可能是我碰到很多问题的重要原因之一,另外没有打框架日志也是一个重要原因。
这次的搭建与上一篇搭建SOAP服务在同一个工程里,主要差别是引入的jar包、接口、服务发布配置不同,另外使用了数据库,建了1个简单的表,web.xml文件没有任何改动。
1.引入jar包,最好的方式仍然是放到lib目录下,这里除了上一篇的搭建SOAP服务引入的包之外就全都是搭建REST服务使用的,主要来源是Spring、CXF以及第三方的包,如jackson的,能够自动转换Object与json格式
CXF的REST支持:cxf-rt-frontend-jaxrs-{version}.jar
@GET、@POST、@Produces、@Consumes等注解的包:javax.ws.rs-api-{version}.jar
jackson的包支持自动转换对象与json:jackson-core-{version}.jar、jackson-annotations-{version}.jar、jackson-databind-{version}.jar、jackson-jaxrs-base-{version}.jar、jackson-jaxrs-json-provider-{version}.jar
2.web.xml文件(与搭建SOAP服务一样,没有任何修改)
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring.xml</param-value><!-- -->
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>cxf</servlet-name>
<servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>cxf</servlet-name>
<url-pattern>/ws/*</url-pattern>
</servlet-mapping>
3.创建接口
这里给接口加了很多注解,@Consumes代表一个资源可以接受的MIME类型,@Produces代表一个资源可以返回的资源类型。这里的GET服务使用路径的方式访问,常见的还有在浏览器通过?后带参数的方式访问,此时把接口定义的路径和参数注解改一下,比如
package com.weijie.rest.api;
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.core.MediaType;
import com.weijie.accounts.Account;
//
@Path("/public/account")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface IAccountService {
@POST
@Path("/fetch")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public String fetch(String uid);
//public Account[] fetch(Account account);
@POST
@Path("/create")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Account create(Account account);
@GET
@Path("/hello/{name}")//使用路径的方式访问
public String sayHello(@PathParam("name") String name);
/*@GET
@Path("/hello")//使用?后加参数的方式访问
public String sayHello(@QueryParam("name") String name);//这里的注解和上面的不一样*/
}
这里仅提供了GET和POST请求,另有一个非自定义类型(String)的GET服务,作为基础已经够简单明了。
关于HTTP协议的几个动作解释如下,更详细的应该学习一下HTTP协议。
GET(SELECT):从服务器取出资源(一项或多项)。
POST(CREATE):在服务器新建一个资源。
PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
DELETE(DELETE):从服务器删除资源。
举例如下:
GET /zoos:列出所有动物园
POST /zoos:新建一个动物园
GET /zoos/ID:获取某个指定动物园的信息
PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)
PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)
DELETE /zoos/ID:删除某个动物园
GET /zoos/ID/animals:列出某个指定动物园的所有动物
DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物
4.实现接口
这里的DBHelper是自己实现的一个类,提供根据读取配置文件来获取数据库连接的方法,还提供一个方法传入驱动串和sql返回ResultSet。这里还没引入mybatis,所以sql是直接写的,另外找了个oracle,创建了1个表,测试了插入和查询数据。Gson是谷歌的一个json包,能够将Object转换为json格式的串,这里主要用于测试返回,后面主要在客户端使用。
package com.weijie.rest.impl;
import com.weijie.rest.api.IAccountService;
import com.weijie.accounts.Account;
import com.weijie.accounts.AccountUtils;
import com.weijie.DBHelper.DBHelper;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import org.apache.log4j.Logger;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public class AccountService implements IAccountService{
private Logger log = Logger.getLogger(AccountService.class);
//private Logger log = Logger.getLogger("AccountService");
public Account create(Account account)
{
if(AccountUtils.isEmpty(account)) return null;
DBHelper helper = new DBHelper("iexcel_sit_url","iexcel_sit_user","iexcel_sit_pwd");
String driver = "oracle.jdbc.driver.OracleDriver";
String sql = "insert into weijie_account_t (id,userid,password,telephonenumber,email,created_by,create_date) "
+ "values(weijie_account_id_sequence_s.nextval,'"+account.getUid()+"','huawei','1008612','10086@140.com','test3',sysdate)";
String sql1 = "select * from weijie_account_t where userid = '" + account.getUid() +"'";
helper.executeSQL(driver, sql);
ResultSet result = helper.executeSQL(driver,sql1);
Account temp = resultSet2Account(result)[0];
return temp ;
}
public String sayHello(String name)
{
return "Hello World " + name;
}
public String fetch(String uid)
{
Account account = new Account();
account.setUid(uid);
Account[] result = null;
Gson gson = new GsonBuilder().create();
result = fetch(account);
String ss = gson.toJson(result[0]);
System.out.println(result[0].getEmail());
return ss;
}
public Account[] fetch(Account account)
{
log.info("entry AccountService");
Account[] result ;
result = new Account[10];
try{
Class.forName("oracle.jdbc.driver.OracleDriver");
Connection conn = DBHelper.getConnectionByProp("iexcel_sit_url", "iexcel_sit_user", "iexcel_sit_pwd");
String sql = "select * from iexcel.weijie_account_t where userid =?";
String userid = account.getUid();
PreparedStatement pstmt = null;
ResultSet resultSet = null;
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, userid);
resultSet = pstmt.executeQuery();
if(resultSet != null)
{
int i = 0;
Account temp = new Account();
try{
while(resultSet.next())
{
ResultSetMetaData metaData = resultSet.getMetaData();
String column4 = metaData.getColumnLabel(4);
temp.setTelephonenumber(resultSet.getString(column4));
temp.setEmail(resultSet.getString(metaData.getColumnLabel(5)));
result[i] = temp;
System.out.println(result[i].toString());
i++;
}
}catch(SQLException e){
e.printStackTrace();
}
}
}catch(ClassNotFoundException e){
e.printStackTrace();
}catch(SQLException e){
e.printStackTrace();
}
return result;
}
public static Account[] resultSet2Account(ResultSet result)
{
if(result == null) return null;
Account[] account;
int num = 0;/*
try{
result.last();
num = result.getRow();
result.first();
}catch(SQLException e){
e.printStackTrace();
}*/
account = new Account[10];//num
try{
if(result !=null)
{
int i=0;
ResultSetMetaData metaData = result.getMetaData();
while(result.next())
{
account[i] = new Account();
account[i].setUid(result.getString(2));
account[i].setTelephonenumber(result.getString(metaData.getColumnLabel(4)));
account[i].setEmail(result.getString(metaData.getColumnLabel(5)));
}
}
}catch(SQLException e){
e.printStackTrace();
}
return account;
}
}
5.发布服务
spring.xml文件的配置,前面引入了REST相关的xmlns等,配置了REST服务的发布,主要差异在引入了CXF的输入输出日志拦截器,在本地测试时会打印出客户端请求的数据,以及服务端的返回数据,方便调试定位问题,在引入前根据网上的各种帖子尝试了很久都没有调通。其中com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider是Json格式自动转换的Provider,比较好用,CXF也有自带的,我在网上找到的大多数博客都是用的这个,推荐一个:http://www.cnblogs.com/zjm701/p/6845813.html写的非常好,但是与上一篇SOAP同样的问题,不需要引入CXF的配置文件,估计是使用较老版本的spring和CXF吧。
<?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:context="http://www.springframework.org/schema/context"
xmlns:jaxws="http://cxf.apache.org/jaxws"
xmlns:jaxrs="http://cxf.apache.org/jaxrs"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://cxf.apache.org/jaxws
http://cxf.apache.org/schemas/jaxws.xsd
http://cxf.apache.org/jaxrs
http://cxf.apache.org/schemas/jaxrs.xsd">
<!--
<bean id="jaxWsServiceFactoryBean" class="org.apache.cxf.jaxws.support.JaxWsServiceFactoryBean">
<property name="wrapped" value="true" />
</bean>-->
<!-- SOAP服务部分配置 -->
<jaxws:endpoint id="IExcelExport" implementor="com.weijie.service.impl.ExcelExport" address="/IExcelExport">
<!-- <jaxws:serviceFactory>
<ref bean="jaxWsServiceFactoryBean" />
</jaxws:serviceFactory>-->
</jaxws:endpoint>
<!-- REST服务部分配置 -->
<bean id="loggingInInterceptor" class="org.apache.cxf.interceptor.LoggingInInterceptor"/>
<bean id="loggingOutInterceptor" class="org.apache.cxf.interceptor.LoggingOutInterceptor"/>
<jaxrs:server address="/rest">
<jaxrs:serviceBeans>
<ref bean="IAccountService"/>
</jaxrs:serviceBeans>
<jaxrs:providers>
<bean class="com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider"/>
</jaxrs:providers>
<jaxrs:inInterceptors>
<ref bean="loggingInInterceptor"/>
</jaxrs:inInterceptors>
<jaxrs:outInterceptors>
<ref bean="loggingOutInterceptor"/>
</jaxrs:outInterceptors>
</jaxrs:server>
<bean id="IAccountService" class="com.weijie.rest.impl.AccountService"></bean>
</beans>
此时配置了多个路径,那么服务的访问地址即是http://IP:port/项目名/CXF的Servlet地址/服务发布地址/接口配置的路径/方法名/参数
6.客户端访问
可以使用HttpClient或者HttpURLConnection来访问,如下示例使用HttpURLConnection来访问,先写一个通用的HttpURLConnection测试类,然后再传参将会很方便
package com.weijie.util;
import java.net.URL;
import java.net.HttpURLConnection;
import java.io.OutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.IOException;
public class HttpURLConnectionTest {
private String serviceURL;
public HttpURLConnectionTest(String serviceURL)
{
this.serviceURL = serviceURL;
}
public String postTest(String data)
{
return request("","POST",data);
}
public String request(String params, String method, String data)
{
HttpURLConnection conn = null;
OutputStream os = null;
try{
URL url = new URL(serviceURL+params);
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod(method);
conn.setRequestProperty("Content-Type", "application/json");
conn.setUseCaches(false);
conn.setDoOutput(true);
if(data !=null && data.trim() != "")
{
os = conn.getOutputStream();
os.write(data.getBytes("UTF-8"));
}
}catch(IOException e){
e.printStackTrace();
}finally{
if(os !=null){
try{
os.close();
}catch(IOException e){
}
}
}
InputStream is = null;
try{
is = conn.getInputStream();
}catch(IOException e){
System.err.println(e);
is = conn.getErrorStream();
}
try{
int code = conn.getResponseCode();
System.out.println(code);
}catch(IOException e){
}
StringBuilder sb = new StringBuilder();
InputStreamReader isr = new InputStreamReader(is);//字节流转换为字符流
BufferedReader br = new BufferedReader(isr);//BufferedReader需要父类型作为参数
try{
String read = br.readLine();//readLine()是BufferedReader独有的方法,
while(read !=null)
{
sb.append(read);
read = br.readLine();
}
}catch(IOException e){
e.printStackTrace();
}finally{
if(is !=null){
try{
is.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
return sb.toString();
}
}
传参并发送请求,这里使用了Gson将Account对象转换为json格式字符串再发送,非常方便。此时我们要回顾一下服务端的接口,明明是Account类型,而并非是字符串类型,却能够成功地调用并返回,说明JsonProvider生效了,如果传输的不是json格式,在服务端将会抛出异常,便能很方便调试。这里同样也能够看出客户端调用的服务地址的结构。
port com.weijie.accounts.Account;
import org.junit.Test;
import com.weijie.util.HttpClientTest;
import com.weijie.util.HttpURLConnectionTest;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public class AccountServiceTest {
public String fetch = "http://10.65.171.219:8080/MySecondWeb/ws/rest/public/account/fetch";
public String create = "http://10.65.171.219:8080/MySecondWeb/ws/rest/public/account/create";
@Test
public void testFetch()
{
Account account = new Account();
account.setUid("user01");
String result =null;
HttpClientTest clientTest = new HttpClientTest(fetch,account);
//clientTest.post();
HttpURLConnectionTest URLTest = new HttpURLConnectionTest(fetch);
Gson gson = new GsonBuilder().create();
result = URLTest.postTest("user01");//gson.toJson(account)
System.out.println(result);
}
@Test
public void testCreate()
{
Account account = new Account();
account.setUid("user04");
account.setEmail("10086@141.com");
String result =null;
/*HttpClientTest clientTest = new HttpClientTest(create,account);
clientTest.post();*/
HttpURLConnectionTest URLTest = new HttpURLConnectionTest(create);
Gson gson = new GsonBuilder().create();
result = URLTest.postTest(gson.toJson(account));//gson.toJson(account)
System.out.println(result);
}
}
路径方式访问GET请求
?后带参数的GET请求访问
由此REST服务搭建的神秘面纱已经被揭下,REST服务的客户端比起SOAP服务要更加的简单,后续会进一步的优化,使过程更加通俗易懂,同时附上解释。