Taylor Durrer
Line-bot-sdk-java
前一陣子都在研究 line-bot-sdk-java,
看到 Retrofit2 搭配 OKHttp3,
媽媽就問我為什麼跪著寫程式。
Simsimi
Simsimi 聽說背後是百度的 NLP 技術的東西, 啊災...
因為我接完後, 他都回我幹話跟髒話, 我就決定叫他髒小雞
,
他會根據一些關鍵字或什麼 NLP 之類的 (太難了) 來回我話, 支援多國語言,
對剛接觸 bot 來說, 算是滿方便, 又不用傷腦力去處理 NLP 的好服務, 當然你送的資料有可能都送給百度啦, maybe
Spring boot Framework
Spring boot framework 算是對新手滿友善的框架?那文件的厚實度, 印出來當枕頭都沒問題。
俗話說, 在哪裡跌倒, 就在哪裡躺下, 就在哪裡爬起來, Bean 很好用, 但有時候找不到文件就很雞掰。
從 Line-bot-sdk-java
的架構下了解一下 java 的注入, 營養真是滿分啊。
Purpose
包裝完後可以像這樣簡單俐落的使用。
Response simSimiResponse = simSimiService.chat("Hi~").execute();
RESTClient Service
Retrofit2 的起手式就是先墊一個 interface
,透過 annonation 就可以很清楚的明暸 Resource,
滿符合 RESTful 的精神的啊, 在閱讀跟維護上也是好棒棒。
詳細的Retrofit2 Doc 請參閱這邊 https://square.github.io/retrofit/
public interface SimSimiService {
/**
* @param msg
* @return ResponseBody {@link ResponseBody}
* @see http://developer.simsimi.com/api
*/
@GET("request.p")
Call chat(@Query("text") String msg);
}
Implement Interface
line-idk-java 採用了 Builder Design Pattern
, 不太確定是不是這樣稱呼?
因為 RESTClient 會有固定定的 Host,固定 Authorized token 跟一些 Header 欄位,
所以在組裝 HTTPClient 的時候採用 Builder 的方式比較容易,
比如說可以當 Api Url 更新時, 可以透過 builder 從外部注入。
Builder Design Pattern 範例 - https://sourcemaking.com/design_patterns/builder/java/2
Builder Design Pattern 說明 - http://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/builder.html
/**
* SimSimiServiceBuilder
* Builder with OkHttpClient Builder and Retrofit Builder
*/
public class SimSimiServiceBuilder {
// 預設 api endpoint url
public static final String DEFAULT_API_END_POINT = "http://sandbox.api.simsimi.com";
// http 預設 connection 常數
public static final long DEFAULT_CONNECT_TIMEOUT = 10_000;
public static final long DEFAULT_READ_TIMEOUT = 10_000;
public static final long DEFAULT_WRITE_TIMEOUT = 10_000;
private String apiEndPoint = DEFAULT_API_END_POINT;
private long connectTimeout = DEFAULT_CONNECT_TIMEOUT;
private long readTimeout = DEFAULT_READ_TIMEOUT;
private long writeTimeout = DEFAULT_WRITE_TIMEOUT;
// okhttp 的攔截器
private List interceptors = new ArrayList<>();
private OkHttpClient.Builder okHttpClientBuilder;
private Retrofit.Builder retrofitBuilder;
// singleton
private SimSimiServiceBuilder(List interceptors) {
this.interceptors.addAll(interceptors);
}
/**
* Create a new {@link SimSimiServiceBuilder}
*/
public static SimSimiServiceBuilder create() {
return new SimSimiServiceBuilder(defaultInterceptors());
}
/**
* 預設的攔截器, 放入 Log 攔截器,
* Log Request 跟 Resoponse 相關資訊
*/
private static List defaultInterceptors() {
final Logger slf4jLogger = LoggerFactory.getLogger("tw.com.dum");
final HttpLoggingInterceptor httpLoggingInterceptor =
new HttpLoggingInterceptor(message -> slf4jLogger.info("{}", message));
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
return Arrays.asList(
new HeaderInterceptor(),
httpLoggingInterceptor
);
}
/**
* RetrofitBuilder
*/
private static Retrofit.Builder createDefaultRetrofitBuilder() {
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// Register JSR-310(java.time.temporal.*) module and read number as millsec.
objectMapper.registerModule(new JavaTimeModule())
.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
return new Retrofit.Builder()
.addConverterFactory(JacksonConverterFactory.create(objectMapper));
}
/**
* 允許彈性的更改 api end point
* Set apiEndPoint.
*/
public SimSimiServiceBuilder apiEndPoint(@NonNull String apiEndPoint) {
this.apiEndPoint = apiEndPoint;
return this;
}
/**
* 允許彈性的更改 connect timeout
* Set connectTimeout in milliseconds.
*/
public SimSimiServiceBuilder connectTimeout(long connectTimeout) {
this.connectTimeout = connectTimeout;
return this;
}
/**
* 允許彈性的更改 read timeout
* Set readTimeout in milliseconds.
*/
public SimSimiServiceBuilder readTimeout(long readTimeout) {
this.readTimeout = readTimeout;
return this;
}
/**
* 允許彈性的更改 write timeout
* Set writeTimeout in milliseconds.
*/
public SimSimiServiceBuilder writeTimeout(long writeTimeout) {
this.writeTimeout = writeTimeout;
return this;
}
/**
* 允許彈性的增加 Interceptor
* Add interceptor
*/
public SimSimiServiceBuilder addInterceptor(Interceptor interceptor) {
this.interceptors.add(interceptor);
return this;
}
/**
* 允許彈性的增加 First Interceptor
* Add interceptor first
*/
public SimSimiServiceBuilder addInterceptorFirst(Interceptor interceptor) {
this.interceptors.add(0, interceptor);
return this;
}
/**
* If you want to use your own setting, specify {@link OkHttpClient.Builder} instance.
*/
public SimSimiServiceBuilder okHttpClientBuilder(@NonNull OkHttpClient.Builder okHttpClientBuilder) {
this.okHttpClientBuilder = okHttpClientBuilder;
return this;
}
/**
* If you want to use your own setting, specify {@link Retrofit.Builder} instance.
* ref: {@link LineMessagingServiceBuilder#createDefaultRetrofitBuilder()} ()}.
*/
public SimSimiServiceBuilder retrofitBuilder(@NonNull Retrofit.Builder retrofitBuilder) {
this.retrofitBuilder = retrofitBuilder;
return this;
}
/**
* Creates a new {@link SimSimiService}.
*/
public SimSimiService build() {
// check and set HttpClient Builder
if (okHttpClientBuilder == null) {
okHttpClientBuilder = new OkHttpClient.Builder();
interceptors.forEach(okHttpClientBuilder::addInterceptor);
}
okHttpClientBuilder
.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
.readTimeout(readTimeout, TimeUnit.MILLISECONDS)
.writeTimeout(writeTimeout, TimeUnit.MILLISECONDS);
final OkHttpClient okHttpClient = okHttpClientBuilder.build();
// check and set Retrofit Builder
if (retrofitBuilder == null) {
retrofitBuilder = createDefaultRetrofitBuilder();
}
retrofitBuilder.client(okHttpClient);
retrofitBuilder.baseUrl(apiEndPoint);
final Retrofit retrofit = retrofitBuilder.build();
// Retrofit implements SimSimiService interface
return retrofit.create(SimSimiService.class);
}
}
/**
* 這邊用 攔截器 替 Request 附加 Header
* 附加 User-Agent 或是一些固定會用的 Header
*/
class HeaderInterceptor implements Interceptor {
// user_agent, 在 http 通訊時告知自己的身份, 一堆爬蟲都會偽造自己是瀏覽器, 也都會改這個欄位
private static final String USER_AGENT =
"my-botsdk-java/" + HeaderInterceptor.class.getPackage().getImplementationVersion();
// channelToken 是 line-bot-sdk 必備的 header
private final String channelToken;
HeaderInterceptor(String channelToken) {
this.channelToken = channelToken;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer " + channelToken)
.addHeader("User-Agent", USER_AGENT)
.build();
return chain.proceed(request);
}
}
在 defaultInterceptors()
裡面有個 new HeaderInterceptor()
,
當Request 透過發送出去的時候,你可以自定義自己的 Interceptor(攔截器),
去添加過濾或者做一些手腳, 比如說 token verify 的動作或 secret key 加密之類的,
透過 Interceptor 來處理, 就不用在商業邏輯處理這些鳥事。
Singleton Bean
早期的 Spring 的 Bean
都是要寫一堆臭臭的 xml,
在某個版本之後透過 @Configuration
的方式就可以取代臭臭的 xml,
是我比較喜歡的做法, Bean 的 Dependency Injection 特性,
可以讓我們要在商業邏輯中使用 SimSimiService
的時候,
只要給個 @Inject
或是 @Autowired
就可以實例化後注入。
public class MyBusinessService {
private SimSimiService simService;
@Inject
public MyBusinessService(SimSimiService simService) {
this.simService = simService;
}
}
在 Configuration
裡面, 我放了 TRIAL_KEY
其實是可以從外部 application.yml
讀進來,
defaultQueryInterceptor
也應該從外面注入, 可以讓 Configuration 更單純一點。
/**
* Created by jerry on 2017/1/9.
*/
@Configuration
public class Configuration {
private static final String TRIAL_KEY = "foo-bar";
/**
* 沒有 zh-tw 嗚嗚嗚...
* @see http://developer.simsimi.com/lclist
*/
private static final String LOCATION_LAN = "ch";
/**
* 設定講髒話的程度, 忘了多少到多少
*/
private static final String BAD_WORD_DISCRIMINATOR = "1.0";
@Bean
public SimSimiService simSimiService() {
return SimSimiServiceBuilder
.create()
.addInterceptor(defaultQueryInterceptor())
.build();
}
/**
* 給定預設的 query, key, lc, ft
*
* @return
*/
private Interceptor defaultQueryInterceptor() {
return chain -> {
HttpUrl keyQuery = chain.request().url()
.newBuilder()
.addQueryParameter("key", TRIAL_KEY)
.addQueryParameter("lc", LOCATION_LAN)
.addQueryParameter("ft", BAD_WORD_DISCRIMINATOR)
.build();
Request request = chain.request().newBuilder().url(keyQuery).build();
return chain.proceed(request);
};
}
}
測試
寫到這邊應該就可以寫個簡單的測試。
因為 Retrofit 用了 Callables 的方式來包裝每個 REST Request,
Callables 是是 Runnable 的進階版, 多允許在 multi-threads 下回傳 result,
這方便我們處理 Request 成功或失敗。
/**
* Created by jerry on 2017/1/5.
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class SimSimServiceTest {
/**
* 注入的另一個好處是讓測試好方便
*/
@Inject
private SimSimiService simSimService;
/**
* Assert Response Code is 200
*/
@Test
public void simSimiApiTest() throws Exception {
Response response = simSimService.chat("HI").execute();
Assert.assertEquals(200, response.code());
}
}
Batter than batter
雖然這樣在使用上已經很方便了, 但看到 line-bot-sdk 又多包了一層 servieClient 的東西,
多包這一層的用意是方便 DTO (Data Transfer Object) 的操作,
這邊細節又可以談很多, 下一篇吧。
沒有留言:
張貼留言