HttpClient POST - Spring Boot 168 EP 22-1

HttpClient POST – Spring Boot 168 EP 22-1

串接第三方 API,使用 HttpClient HttpPost 發出 POST 請求,登入或登出系統,返回 HTTP 200 成功,接收所提供的 ErrorCode、Token 等資訊, EP 22-1 增加了範例 ,並透過 JUnit 5 來驗證產出結果。

前言

HttpClient 是一套支援 HTTP 協議的用戶端程式庫,實現了所有 HTTP 的方法,如: GET 、 POST 、PUT 等,以及支援自動轉向與代理服務器等,提供了許多高效率的類別。

HttpClient POST

檔案目錄

./
   +- build.gradle
       +- src
           +- main
               +- java
                   +- org
                       +- ruoxue
                           +- spring_boot_168
                               +- game
                                   +- ggg
                                       +- client
                                       |   +- GggClient.java 
                                       +- ex
                                       |   +- GggException.java 
                                       +- model
                                       |   +- GggReponse.java 

設定

Java HttpClient POST JSON

網址:http://ggg.cc:10090
Function Method Path Content-Type Params Description
新增使用者 POST /user application/json;charset=UTF-8 username 使用者名稱
password 密碼
Reponse {"errorCode":0,"name":"player","token":null}
 
使用者登入 POST /login application/json;charset=UTF-8 username 使用者名稱
password 密碼
Reponse {"errorCode":0,"name":"player","token":"0x12345678"}
 
使用者登出 POST /logout application/x-www-form-urlencoded username 使用者名稱
token Token
Reponse {"errorCode":0}
模擬 API,參考此篇:
注入 CloseableHttpClient,如何設定,參考此篇:

實作

GggReponse.java

新增檔案,接收回應,定義 errorCode 、 token 、 name 。

package org.ruoxue.spring_boot_168.game.ggg.model;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
public class GggReponse {
	/** 錯誤碼 */
	private int errorCode;
	/** Token */
	private String token;
	/** 名稱 */
	private String name;

	@Override
	public String toString() {
		ToStringBuilder builder = new ToStringBuilder(this, ToStringStyle.JSON_STYLE);
		builder.appendSuper(super.toString());
		builder.append("errorCode", errorCode);
		builder.append("token", token);
		builder.append("name", name);
		return builder.toString();
	}
}

GggException.java

新增檔案,自定義例外,當發生錯誤時,拋出此例外,讓外層調用的服務,處理例外,如:記錄日誌。

package org.ruoxue.spring_boot_168.game.ggg.ex;

public class GggException extends RuntimeException {

	private static final long serialVersionUID = 2209749235554430258L;

	public GggException() {
		super();
	}

	public GggException(String message) {
		super(message);
	}

	public GggException(Throwable cause) {
		super(cause);
	}

	public GggException(String message, Throwable cause) {
		super(message, cause);
	}
}

HttpClient HttpPost

StringEntity 傳入 JSON 字串。

Apache HttpPost

UrlEncodedFormEntity 傳入 NameValuePair 。

GggClient.java

新增檔案,調用第三方 API 客戶端。

package org.ruoxue.spring_boot_168.game.ggg.client;

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

import org.apache.commons.lang3.StringUtils;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.ruoxue.spring_boot_168.game.ggg.ex.GggException;
import org.ruoxue.spring_boot_168.game.ggg.model.GggReponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;

import com.google.gson.Gson;
import com.google.gson.JsonObject;

import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
public class GggClient {

	/** API 網址 */
	public static final String API_URL = "http://ggg.cc:10090";
	/** 使用者資訊 */
	public static final String USER = "/user";
	/** 使用者登入 */
	public static final String LOGIN = "/login";
	/** 使用者登出 */
	public static final String LOGOUT = "/logout";

	@Autowired
	@Qualifier("closeableHttpClient")
	private CloseableHttpClient httpClient;

	private static final Gson gson = new Gson();

	/**
	 * 新增使用者
	 * 
	 * Content-Type: application/json;charset=UTF-8
	 * 
	 * @param username
	 * @param password
	 * @return
	 * @throws Exception
	 */
	public GggReponse create(String username, String password) throws Exception {
		GggReponse result = null;
		try {
			String requestUrl = API_URL + USER;
			HttpPost httpPost = new HttpPost(requestUrl);
			httpPost.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);

			JsonObject params = new JsonObject();
			params.addProperty("username", username);
			params.addProperty("password", password);

			StringEntity stringEntity = new StringEntity(params.toString(), "UTF-8");
			httpPost.setEntity(stringEntity);
			ResponseHandler<String> responseHandler = response -> {
				int status = response.getStatusLine().getStatusCode();
				if (status >= HttpStatus.SC_OK && status < HttpStatus.SC_MULTIPLE_CHOICES) {
					HttpEntity entity = response.getEntity();
					return (entity != null ? EntityUtils.toString(entity) : null);
				} else {
					log.error("statusCode: " + status);
					log.error("statusLine: " + response.getStatusLine());
					throw new ClientProtocolException("Unexpected response status: " + status);
				}
			};
			log.info("requestUrl: " + requestUrl);
			String body = httpClient.execute(httpPost, responseHandler);
			if (StringUtils.isNotEmpty(body)) {
				result = gson.fromJson(body, GggReponse.class);
			} else {
				throw new GggException("ERRORS_NOT_EXIST");
			}
		} catch (Exception ex) {
			throw ex;
		}
		return result;
	}
	
	/**
	 * 使用者登入
	 * 
	 * Content-Type: application/json;charset=UTF-8
	 * 
	 * @param username
	 * @param password
	 * @return
	 * @throws Exception
	 */
	public GggReponse login(String username, String password) throws Exception {
		GggReponse result = null;
		try {
			String requestUrl = API_URL + LOGIN;
			HttpPost httpPost = new HttpPost(requestUrl);
			httpPost.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);

			JsonObject params = new JsonObject();
			params.addProperty("username", username);
			params.addProperty("password", password);

			StringEntity stringEntity = new StringEntity(params.toString(), "UTF-8");
			httpPost.setEntity(stringEntity);
			ResponseHandler<String> responseHandler = response -> {
				int status = response.getStatusLine().getStatusCode();
				if (status >= HttpStatus.SC_OK && status < HttpStatus.SC_MULTIPLE_CHOICES) {
					HttpEntity entity = response.getEntity();
					return (entity != null ? EntityUtils.toString(entity) : null);
				} else {
					log.error("statusCode: " + status);
					log.error("statusLine: " + response.getStatusLine());
					throw new ClientProtocolException("Unexpected response status: " + status);
				}
			};
			log.info("requestUrl: " + requestUrl);
			String body = httpClient.execute(httpPost, responseHandler);
			if (StringUtils.isNotEmpty(body)) {
				result = gson.fromJson(body, GggReponse.class);
			} else {
				throw new GggException("ERRORS_NOT_EXIST");
			}
		} catch (Exception ex) {
			throw ex;
		}
		return result;
	}

	/**
	 * 使用者登出
	 * 
	 * Content-Type: application/x-www-form-urlencoded
	 * 
	 * @param username
	 * @param token
	 * @return
	 * @throws Exception
	 */
	public GggReponse logout(String username, String token) throws Exception {
		GggReponse result = null;
		try {
			String requestUrl = API_URL + LOGOUT;
			HttpPost httpPost = new HttpPost(requestUrl);
			httpPost.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);

			List<NameValuePair> params = new ArrayList<NameValuePair>();
			params.add(new BasicNameValuePair("username", username));
			params.add(new BasicNameValuePair("token", token));

			UrlEncodedFormEntity urlEntity = new UrlEncodedFormEntity(params, Consts.UTF_8);
			httpPost.setEntity(urlEntity);
			ResponseHandler<String> responseHandler = response -> {
				int status = response.getStatusLine().getStatusCode();
				if (status >= HttpStatus.SC_OK && status < HttpStatus.SC_MULTIPLE_CHOICES) {
					HttpEntity entity = response.getEntity();
					return (entity != null ? EntityUtils.toString(entity) : null);
				} else {
					log.error("statusCode: " + status);
					log.error("statusLine: " + response.getStatusLine());
					throw new ClientProtocolException("Unexpected response status: " + status);
				}
			};
			log.info("requestUrl: " + requestUrl);
			String body = httpClient.execute(httpPost, responseHandler);
			if (StringUtils.isNotEmpty(body)) {
				result = gson.fromJson(body, GggReponse.class);
			} else {
				throw new GggException("ERRORS_NOT_EXIST");
			}
		} catch (Exception ex) {
			throw ex;
		}
		return result;
	}
}

測試 JUnit 5​

GggClientTest.java

新增單元測試,驗證是否符合預期 。

package org.ruoxue.spring_boot_168.game.ggg.client;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.ruoxue.spring_boot_168.Application;
import org.ruoxue.spring_boot_168.game.ggg.model.GggListReponse;
import org.ruoxue.spring_boot_168.game.ggg.model.GggReponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = Application.class)
public class GggClientTest {

	@Autowired
	private GggClient client;

	@Test
	public void gggClient() {
		System.out.println(client);
		assertNotNull(client);
	}

	@Test
	public void create() throws Exception {
		GggReponse gggReponse = client.create("ruoxue", "1111");
		System.out.println(gggReponse);
		assertNotNull(gggReponse);
		assertEquals(0, gggReponse.getErrorCode());
	}

	
	@Test
	public void login() throws Exception {
		GggReponse gggReponse = client.login("ruoxue", "1111");
		System.out.println(gggReponse);
		assertNotNull(gggReponse);
		assertEquals(0, gggReponse.getErrorCode());
		assertEquals("0x12345678", gggReponse.getToken());
	}

	@Test
	public void logout() throws Exception {
		GggReponse gggReponse = client.logout("ruoxue", "0x12345678");
		System.out.println(gggReponse);
		assertNotNull(gggReponse);
		assertEquals(0, gggReponse.getErrorCode());
	}
}

create

測試方法上點右鍵執行 Run As -> JUnit Test ,查看 console 。

2022-07-09T13:39:03.721+0800 [main] INFO GggClient#login:59 - requestUrl: http://ggg.cc:10090/create
{"errorCode":0,"token":null,"name":"player"}

login

測試方法上點右鍵執行 Run As -> JUnit Test ,查看 console 。

2022-07-09T13:39:03.721+0800 [main] INFO GggClient#login:59 - requestUrl: http://ggg.cc:10090/login
{"errorCode":0,"token":"0x12345678","name":"player"}

logout

測試方法上點右鍵執行 Run As -> JUnit Test ,查看 console 。

2022-07-09T13:39:03.721+0800 [main] INFO GggClient#login:59 - requestUrl: http://ggg.cc:10090/logout
{"errorCode":0,"token":null,"name":null}

心得分享

使用 Nginx 模擬第三方 API,來協助快速開發,從連接池取得連線,建立 Apache HttpPost ,設定 Content-Type ,及增加請求參數, 發送 POST 請求,然後會返回一個 HttpResponse 物件,封裝了 Server 的回應,並且可以透過該物件取得 HTTP 狀態碼,回應內容等。

GitHub - ruoxueorg

發佈留言