某電商平臺(tái)開(kāi)發(fā)記要——客服系統(tǒng)
假如網(wǎng)站需要提供客服功能,如果只是簡(jiǎn)單的聊天咨詢(xún)可以考慮營(yíng)銷(xiāo)QQ、百度商橋等(目前大部分網(wǎng)站采用此方式,包括一些知名行業(yè)電商);如果需要更精細(xì)化的管理,比如客服人員安排、各項(xiàng)數(shù)據(jù)統(tǒng)計(jì)匯總,那么需要對(duì)接專(zhuān)業(yè)的第三方客服平臺(tái),比如網(wǎng)易七魚(yú),當(dāng)然價(jià)格不菲;然而若是如京東本身就是一個(gè)平臺(tái),需要為每個(gè)商家提供各自的客服管理,首先目前第三方提供商并無(wú)此類(lèi)產(chǎn)品(網(wǎng)易七魚(yú)據(jù)說(shuō)已經(jīng)開(kāi)發(fā)出來(lái)了,但是官網(wǎng)上沒(méi)找到),其次即使有,價(jià)格也肯定不便宜,而且數(shù)據(jù)在別人那里總歸不好。所以電商平臺(tái)的客服系統(tǒng),一般都是自己開(kāi)發(fā)。當(dāng)然了,借助優(yōu)秀的開(kāi)源項(xiàng)目,自主開(kāi)發(fā)[一套簡(jiǎn)單能用的]也變得輕松很多。
我采用了openfire+spark+layim,前兩者基于java平臺(tái),layim是國(guó)人開(kāi)發(fā)的一個(gè)webim前端組件。
先來(lái)看大致效果(左邊是瀏覽器layim-客戶(hù)提咨詢(xún),右邊是spark聊天窗口-客服解答)

圖示:

本文涉及到的知識(shí)點(diǎn)(雜亂,后續(xù)會(huì)不定期添加內(nèi)容):
Java基礎(chǔ)
Intellij Idea:Java IDE
Mybatis:半ORM
XMPP協(xié)議
smack:XMPP協(xié)議的Java封裝
openfire
fastpath:openfire插件,我們需要依賴(lài)它實(shí)現(xiàn)客服功能
spark
一秒鐘入門(mén)Java
Java SE(J2SE):Standard Edition,可認(rèn)為是基礎(chǔ)庫(kù),用于開(kāi)發(fā)和部署桌面、服務(wù)器以及嵌入設(shè)備(J2ME)和實(shí)時(shí)環(huán)境中的Java應(yīng)用程序。
Java EE(J2EE):基于SE的高級(jí)庫(kù),提供 Web 服務(wù)、組件模型、管理和通信 API,可以用來(lái)實(shí)現(xiàn)企業(yè)級(jí)的面向服務(wù)體系結(jié)構(gòu)。
可以知道J2EE比J2SE多了Web相關(guān)的組件和API,但是本人在使用SpringMVC框架開(kāi)發(fā)Web應(yīng)用程序時(shí),去官網(wǎng)Java SE頁(yè)面下載的JDK,也能正常開(kāi)發(fā)。后來(lái)查看官網(wǎng)的Java EE的下載頁(yè)面,發(fā)現(xiàn)提供的SDK中主要包含一個(gè)叫GlassFish的開(kāi)源組件和一些示例及文檔,而Java EE剛開(kāi)始是以一種規(guī)范提出,GlassFish可以看作是實(shí)現(xiàn)了這些規(guī)范的JEE容器,而我們開(kāi)發(fā)Web站點(diǎn)時(shí)部署到服務(wù)器(比如Tomcat),實(shí)現(xiàn)了JEE規(guī)范其中的Servlet容器部分,所以以JDK開(kāi)發(fā)Web并不會(huì)出現(xiàn)問(wèn)題。
JNDI 是什么 :簡(jiǎn)單的說(shuō)就是為了解耦,非直接引用,而是通過(guò)名稱(chēng)或地址查找然后加載的方式,常用依賴(lài)注入的方式實(shí)現(xiàn)。
目前流行的IDE有Eclipse和IntelliJ IDEA,前者免費(fèi)且由于歷史關(guān)系占有率一直很高,后者也有社區(qū)版,據(jù)說(shuō)使用性上目前完勝前者。
final關(guān)鍵詞:類(lèi)似于.NET的readonly
匿名內(nèi)部類(lèi):
定義一個(gè)類(lèi)A(可以為abstract),為方便說(shuō)明,在A(yíng)中定義一個(gè)[抽象]方法dosth。在調(diào)用方法里可以直接new A,并且同時(shí)給dosth賦方法體。
public abstract A{ public void dosth() { } } public abstract B{ public void call() { final A a = new A() { public void dosth() { //這里寫(xiě)方法體 } }; } }
看著是實(shí)例化了A的一個(gè)對(duì)象,其實(shí)是實(shí)例化了A類(lèi)的匿名子類(lèi)。
Access restriction:eclipse對(duì)某些java包(or 類(lèi)?)有access rules,比如 sun.awt.shell.ShellFolder。因?yàn)檫@些JAR默認(rèn)包含了一系列的代碼訪(fǎng)問(wèn)規(guī)則(Access Rules),如果代碼中引用了這些訪(fǎng)問(wèn)規(guī)則所禁止引用類(lèi),那么就會(huì)提示這個(gè)錯(cuò)誤信息。解決方法:既然存在訪(fǎng)問(wèn)規(guī)則,那么修改訪(fǎng)問(wèn)規(guī)則即可。打開(kāi)項(xiàng)目的Build Path Configuration頁(yè)面,打開(kāi)引用的[報(bào)錯(cuò)]JAR包,選中Access rules條目,選擇右側(cè)的編輯按鈕,添加一個(gè)訪(fǎng)問(wèn)規(guī)則即可。
Java NIO
Apache Mina
CopyOnWrite:CopyOnWrite容器即寫(xiě)時(shí)復(fù)制的容器。通俗的理解是當(dāng)我們往一個(gè)容器添加元素的時(shí)候,不直接往當(dāng)前容器添加,而是先將當(dāng)前容器進(jìn)行Copy,復(fù)制出一個(gè)新的容器,然后新的容器里添加元素,添加完元素之后,再將原容器的引用指向新的容器。這樣做的好處是我們可以對(duì)CopyOnWrite容器進(jìn)行并發(fā)的讀,而不需要加鎖。從JDK1.5開(kāi)始Java并發(fā)包里提供了兩個(gè)使用CopyOnWrite機(jī)制實(shí)現(xiàn)的并發(fā)容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet。
Maven:項(xiàng)目管理工具。不像VS,eclipse是更純粹的編碼工具,在維護(hù)jar包和項(xiàng)目之間的依賴(lài)關(guān)系、項(xiàng)目的構(gòu)建目標(biāo)等方面的功能比較弱(比如拷貝了一個(gè)項(xiàng)目,我們需要手動(dòng)去Configure Build Path),而Maven就是補(bǔ)足于此。Maven獨(dú)立于IDE,eclipse有一個(gè)插件叫M2E,里面內(nèi)置了Maven。Maven項(xiàng)目的配置信息保存在pom.xml文件中。
我們?cè)趯?dǎo)入Maven項(xiàng)目時(shí),有時(shí)會(huì)發(fā)現(xiàn)不止一個(gè)pom.xml,那是因?yàn)轫?xiàng)目中有子項(xiàng)目(module,module有自己的pom.xml),只要選擇最頂層的pom.xml文件即可,會(huì)自動(dòng)加載引用到的子項(xiàng)目。
JavaBean:可參看 Java Bean 是個(gè)什么概念? (不過(guò)這個(gè)問(wèn)題里有個(gè)答主說(shuō)Java沒(méi)有事件的概念,讓我大吃一驚,不過(guò)轉(zhuǎn)念一想,Java主要用于開(kāi)發(fā)服務(wù)端應(yīng)用,確實(shí)不怎么涉及到[自定義]事件。其實(shí)Java中是有事件機(jī)制的,只是不知變通,就一個(gè)半成品的觀(guān)察者模式,要預(yù)先以類(lèi)的形式定義好各交互方,較為繁瑣(參看Java —— 事件處理機(jī)制),想想C#的委托,其實(shí)就一個(gè)函數(shù)指針的事)
MVC:當(dāng).Neter們?cè)诒籄sp.Net的重量壓得踹不過(guò)氣來(lái)的時(shí)候,Java已經(jīng)有MVC的概念了。很多模式,.Net界都是直接copy,.Neter們并沒(méi)有對(duì)其歷史的認(rèn)知,所以接收不能,MVC就是如此。其實(shí)在A(yíng)sp.Net時(shí)代已經(jīng)有MVC的影子,就是一般處理程序.ashx。很早以前,用戶(hù)提交都是提交到具體的一個(gè)頁(yè)面,于是會(huì)經(jīng)常導(dǎo)致一個(gè)頁(yè)面并不是用于顯示,而是用于業(yè)務(wù)邏輯的處理,于是后來(lái)把業(yè)務(wù)邏輯單獨(dú)拎出來(lái),這便是Controller,用戶(hù)請(qǐng)求的是Controller,不再是具體頁(yè)面,并且Controller里不再使用類(lèi)似HttpRequest或者HttpResponse獲取數(shù)據(jù)和返回響應(yīng),而是使用對(duì)象的形式(M),這便是MVC模式。可參看 Java Web開(kāi)發(fā)模式
Java中的注解相當(dāng)于.NET中的Attribute。
Spring是一個(gè)IOC和AOP框架。我們可以通過(guò)在xml文件中配置bean,然后在代碼中使用@Autowired或@Resource注入bean實(shí)例,不過(guò)配置的環(huán)節(jié)稍顯繁瑣,能不能省略呢?答案是肯定的,Sping2.5開(kāi)始支持注解注入,具體可看 spring注解注入:<context:component-scan>詳解。需要注意的是,@Component及相關(guān)的幾個(gè)注解類(lèi),在應(yīng)用到interface上的時(shí)候,可能并不如預(yù)期工作,因?yàn)閕nterface并不能實(shí)例化,而這幾個(gè)注解類(lèi)貌似又沒(méi)有@Inherited修飾,所以就算有實(shí)現(xiàn)類(lèi)或運(yùn)行時(shí)的動(dòng)態(tài)實(shí)現(xiàn)類(lèi),也不會(huì)注冊(cè)到上下文中;且修飾的類(lèi)要有公共構(gòu)造函數(shù)。另外注入[被注入方]一般只能在注入方本身是已注冊(cè)的bean里,若在普通類(lèi)里想通過(guò)@Autowired或@Resource的方式注入bean,則稍微有點(diǎn)繞,可參看 Java普通類(lèi)獲取Spring XML中Bean的方法總結(jié)
關(guān)于Servlet、Struts、Spring、SpringMVC的關(guān)系與區(qū)別可參看 Java開(kāi)發(fā)web的幾種開(kāi)發(fā)模式 和 SpringMVC與Struts2的對(duì)比
SpringMVC竟然URL和參數(shù)大小寫(xiě)敏感,雖然有辦法配置,但這種預(yù)設(shè)沒(méi)有道理吧。。。
Servlet url-pattern /與/*區(qū)別:兩者的長(zhǎng)度不同,根據(jù)最長(zhǎng)路徑匹配的優(yōu)先級(jí),/*比/更容易被選中,而/的真正含義是,缺省匹配。既所有的URL都無(wú)法被選中的時(shí)候,就一定會(huì)選中/,可見(jiàn)它的優(yōu)先級(jí)是最低的,這就兩者的區(qū)別。
xml文件也可以打包進(jìn)jar包,但是訪(fǎng)問(wèn)jar包里的xml文件就不能按文件目錄的方式來(lái)了,可參看 http://blog.csdn.net/jianxin1009/article/details/18814799
application.getInitParameter:jsp中9個(gè)內(nèi)置對(duì)象之一application,它的數(shù)據(jù)對(duì)整個(gè)web應(yīng)用都有效,application有一個(gè)重要的用途就是通過(guò)getInitParameter()獲取web.xm中的配置參數(shù),這樣可以提高代碼的移植性。
dwr:簡(jiǎn)化ajax調(diào)用,使得調(diào)用遠(yuǎn)程服務(wù)器方法看上去像調(diào)用本地方法一樣。
在java項(xiàng)目中必不可少的是我們要指定一個(gè)jdk。在指定jdk的同時(shí),還可以指定jdk的Language level,這個(gè)有點(diǎn)像我們工程最低支持版本。比如Language level 設(shè)置了5.0 只是就不能出現(xiàn)使用6.0/7.0特性的代碼,因?yàn)檫@些特性在5.0的環(huán)境下是無(wú)法編譯的。或者可以理解ide會(huì)安裝Language level指定的jdk版本來(lái)對(duì)我們的代碼進(jìn)行編譯,以及錯(cuò)誤檢查。即同樣的jdk對(duì)應(yīng)不同的Language Level會(huì)采用[可能]不同的編譯和優(yōu)化方式。
Java中也有類(lèi)似.Net的字符串池的概念,請(qǐng)看 String中intern的方法
Java插件技術(shù): OSGi
貌似在同一package下,protected可見(jiàn)。(和.NET不同)
Java的泛型類(lèi)型只能是引用類(lèi)型,而不能是基礎(chǔ)類(lèi)型,但是Java針對(duì)每個(gè)基礎(chǔ)類(lèi)型有對(duì)應(yīng)的封裝類(lèi)型,比如boolean對(duì)應(yīng)Boolean,后者是引用類(lèi)型,可以為null,當(dāng)封裝類(lèi)型不為null時(shí),可以隱式轉(zhuǎn)換,但寫(xiě)代碼時(shí)null的情況要自己處理,如
private boolean existUser(String username) { Boolean result = null; return result != null && result.booleanValue(); }
Ant:類(lèi)似于.NET的MSBuild,其構(gòu)建文件默認(rèn)為build.xml(可以在其中指定構(gòu)建基于的Java平臺(tái)版本),每個(gè)構(gòu)建文件都對(duì)應(yīng)于一個(gè)項(xiàng)目,但是大型項(xiàng)目經(jīng)常包含大量的子項(xiàng)目,每一個(gè)子項(xiàng)目都可以有自己的構(gòu)建文件。
一個(gè).java文件中可以定義多個(gè)類(lèi),但是public修飾的只能至多有一個(gè),且要與文件名相同,編譯后,有幾個(gè)類(lèi)就會(huì)產(chǎn)生幾個(gè)對(duì)應(yīng)的.class文件。jar包類(lèi)似.Net的dll,它將多個(gè).class文件打包一塊。大多數(shù) JAR 文件包含一個(gè) META-INF 目錄,它用于存儲(chǔ)包和擴(kuò)展的配置數(shù)據(jù),如安全性和版本信息。Java 2 平臺(tái)識(shí)別并解釋 META-INF 目錄中的下述文件和目錄,以便配置應(yīng)用程序、擴(kuò)展和類(lèi)裝載器。具體可看 MANIFEST.MF 文件內(nèi)容完全詳解。
System.getProperty()獲取系統(tǒng)/項(xiàng)目全局變量,比如Java運(yùn)行時(shí)版本,當(dāng)然我們也可以通過(guò)System.setProperty()設(shè)置自定義變量。
Java桌面客戶(hù)端編程:Java Swing 。桌面程序畢竟不是Java的主流領(lǐng)域,因此各IDE貌似也并未作太多努力,相較VS的所見(jiàn)即所得的控件拖拽開(kāi)發(fā)模式,Java GUI編程就吃力很多了。
Java國(guó)際化:i18n,注意中文的資源文件,貌似需要先UTF-8轉(zhuǎn)碼,大約就是像這樣
。(可以使用JDK自帶的native2ascii.exe)
Intellij Idea
使用Intellij Idea創(chuàng)建spring mvc時(shí)(沒(méi)用maven),run都報(bào) Error during artifact deployment. See server log for details 錯(cuò)誤,后來(lái)把lib文件夾拷到WEB-INFO文件夾下就沒(méi)問(wèn)題了,不知何故。
原因:tomcat默認(rèn)是去web-info/lib/下找依賴(lài)的jar包。手動(dòng)拷j(luò)ar包畢竟不是一個(gè)好辦法,其實(shí)我們可以在下圖處進(jìn)行Artifacts設(shè)置

運(yùn)行項(xiàng)目,項(xiàng)目目錄下會(huì)多出一個(gè)out文件夾,生成所有的站點(diǎn)文件,依賴(lài)包會(huì)自動(dòng)拷貝到下面的WEB-INF/lib/下,如下圖:

IDEA配置artifacts中Web Application:Exploded和Web Application:Archive的區(qū)別:前者以文件夾形式(War Exploded)發(fā)布項(xiàng)目,后者以war包形式(每次都會(huì)重新打包全部的)。Tomcat會(huì)自動(dòng)解壓war包并啟動(dòng)站點(diǎn),缺點(diǎn)是會(huì)造成一段時(shí)間的站點(diǎn)不可用,而以文件夾形式發(fā)布的話(huà),則支持熱部署(需進(jìn)行額外的一些配置)。
當(dāng)然我們也可以使用Maven進(jìn)行依賴(lài)包的管理。在當(dāng)前項(xiàng)目右鍵->Add Framework Support->Maven即可。注意需要在Project Structure-> Project Settings中移除之前非Maven引用的包依賴(lài)。此時(shí)運(yùn)行項(xiàng)目,項(xiàng)目目錄下會(huì)多出一個(gè)target文件夾,其下有生成的站點(diǎn)文件。但是運(yùn)行時(shí)發(fā)現(xiàn)WEB-INF下的文件除了web.xml外,其它的文件都不會(huì)覆蓋,貌似用maven管理的web工程,需要將applicationContext.xml等資源文件放在resource目錄下,然后以classpath的方式去訪(fǎng)問(wèn)。后來(lái)發(fā)現(xiàn)jsp頁(yè)面也無(wú)法自動(dòng)更新到target目錄,再后來(lái)聽(tīng)說(shuō)maven有一套約定的目錄結(jié)構(gòu),貌似又可以通過(guò)pom.xml進(jìn)行自定義配置,神煩!目前靠手動(dòng)覆蓋。參考 Maven使用點(diǎn)滴 配置即可(webappDirectory我沒(méi)設(shè)置,就設(shè)置了warSourceDirectory,能正常更新了)
Intellij Idea中有個(gè)Ant Build Window,默認(rèn)顯示的是主項(xiàng)目下build.xml中的targets,and by default, IDEA only shows the default target and targets that have descriptions。對(duì)這個(gè)有疑問(wèn)可參看 How to get Ant Build to list all targets in a hierarchy of build files.
可以在Run/Debug Configurations Window中設(shè)置自定義系統(tǒng)變量,如下圖(-D不能省):

MyBatis
一個(gè)半ORM框架,SQL語(yǔ)句并不是像EF一樣由框架解析,而是要預(yù)先寫(xiě)在xml中或者寫(xiě)在Java注解(同.Net的Attribute)中,且不支持匿名類(lèi)型(即select出來(lái)的數(shù)據(jù)要么是基礎(chǔ)類(lèi)型,要么要有對(duì)應(yīng)的Java Bean)。一般情況下,我們使用resultType映射查詢(xún)結(jié)果和對(duì)象即可(MyBatis 會(huì)在幕后自動(dòng)創(chuàng)建一個(gè) ResultMap),當(dāng)只想映射部分字段或者包含復(fù)雜類(lèi)型屬性的時(shí)候,我們需要自定義ResultMap。
MyBatis不支持方法重載,因?yàn)樗峭ㄟ^(guò)方法名稱(chēng)(不加參數(shù))去查找執(zhí)行方法,因此我們?cè)O(shè)置不同的方法名,或者使用動(dòng)態(tài)sql。
XMPP協(xié)議
JID表示一個(gè)地址,由三部分組成——node、domain和resource。例如:xiaoming@xiaoming.home/sleeping,xiaoming就是node ,xiaoming.home就是domain,sleeping就是resource。node domain 和resource任何一部分都不能超過(guò)1023 字節(jié) ,加上@和 /,一個(gè)JID 總共不能超過(guò)3071字節(jié)。BareJid就是去掉resource,只包含node@domain。
XMPP包含IQ, message and presence 三種packet。
smack
ConnectionConfiguration.Builder的setXmppDomain和setHost的區(qū)別?一個(gè)是域(服務(wù)器集群),一個(gè)是其中的一臺(tái)服務(wù)器,應(yīng)該只要設(shè)置其中一個(gè)就可以了。
使用XMPPTCPConnectionConfiguration建立連接時(shí)報(bào)空指針錯(cuò)誤,調(diào)試發(fā)現(xiàn)有個(gè)base64encoder未賦值,需要引用smack-java7包,該包會(huì)初始化base64encoder,如果是安卓開(kāi)發(fā),那么就引用smack-android。
openfire
使用idea導(dǎo)入openfire代碼,過(guò)程可參考將openfire源碼部署到IDEA中 或者 IntelliJ IDEA搭建openfire4.1.3開(kāi)發(fā)環(huán)境 。使用openfire配置界面只能配置一個(gè)數(shù)據(jù)庫(kù),且我也不打算完全依賴(lài)它生成的數(shù)據(jù)庫(kù)。我需要openfire部分功能使用現(xiàn)有的數(shù)據(jù)庫(kù)(比如用戶(hù)表),而openfire的業(yè)務(wù)數(shù)據(jù)仍然使用生成的數(shù)據(jù)庫(kù),因此涉及到多庫(kù)連接。這只能去修改源碼了。
上面說(shuō)到的配置界面設(shè)置的項(xiàng)最終存儲(chǔ)在ofproperty表中。在配置界面完成配置后,我們也可以在conf/openfire.xml中重新設(shè)置值,重啟openfire,配置文件中的值會(huì)更新到數(shù)據(jù)庫(kù)中。
以AuthFactory為例,其initProvider方法里有 JiveGlobals.migrateProperty("provider.auth.className"); ,XMLProperties根據(jù)"provider.auth.className"讀取xml文件中的值(getProperty方法)
//按逗號(hào)拆分為數(shù)組 String[] propName = parsePropertyName(name); // Search for this property by traversing down the XML hierarchy. Element element = document.getRootElement(); for (String aPropName : propName) { element = element.element(aPropName); if (element == null) { return null; } } value = element.getTextTrim();
對(duì)應(yīng)的配置節(jié)寫(xiě)法如下(可以看到,propName對(duì)應(yīng)各層級(jí)element,而非attribute形式)
<provider> <auth> <className>org.jivesoftware.openfire.auth.JDBCAuthProvider</className> </auth> </provider>
而后覆蓋數(shù)據(jù)庫(kù)值
public void migrateProperty(String name) { if (getProperty(name) != null) { if (JiveGlobals.getProperty(name) == null) { JiveGlobals.setProperty(name, getProperty(name)); deleteProperty(name); } else if (JiveGlobals.getProperty(name).equals(getProperty(name))) { deleteProperty(name); } else if (!JiveGlobals.getProperty(name).equals(getProperty(name))) { Log.warn("XML Property '"+name+"' differs from what is stored in the database. Please make property changes in the database instead of the configuration file."); } } }
當(dāng)然,若是我們有數(shù)據(jù)庫(kù)權(quán)限,直接進(jìn)入數(shù)據(jù)庫(kù)修改也一樣。
openfire源碼采用JDBC方式操作數(shù)據(jù)庫(kù),而且沒(méi)有做很好的封裝,重復(fù)代碼較多,如下圖所示

相似代碼在與數(shù)據(jù)庫(kù)交互的地方隨處可見(jiàn)。部分邏輯的抽取,莫過(guò)于lambda(回調(diào)函數(shù))的方式。考慮到Java8已經(jīng)支持lambda表達(dá)式,重構(gòu)如下:
public <T> T excuteQuery(String queryText, Function<ResultSet, T> func) { T result = null; Connection con = null; PreparedStatement pstmt = null; ResultSet rs = null; try { con = getConnection(); pstmt = con.prepareStatement(queryText); rs = pstmt.executeQuery(); if (rs.next()) { result = func.apply(rs); } } catch (SQLException e) { Log.error(e.getMessage(), e); } finally { DbConnectionManager.closeConnection(rs, pstmt, con); } return result; }
但是在寫(xiě)調(diào)用代碼的時(shí)候提示:

雖然我們?cè)趀xcuteQuery方法中已經(jīng)catch了這個(gè)異常,但是編譯器并不買(mǎi)賬。而且就算我們?cè)诜椒ǘx時(shí)已經(jīng)throws了相關(guān)異常,也沒(méi)用,如下圖:

解決方法有兩種:可以在lambda體內(nèi)catch異常后不再throw;或者自定義一個(gè)Functional Interface,其中聲明一個(gè)定義了異常的方法,
@FunctionalInterface public interface CheckedSQLExceptionFunction<T, R> { R apply(T t) throws SQLException; }
然后將Function<Result,T>的地方替換為CheckedSQLExceptionFunction<ResultSet, T>。這兩種都顯得別扭與不合理,導(dǎo)致這一問(wèn)題的是,Java Lambda規(guī)定如果Lambda中拋出了異常,那么這個(gè)異常一定要在Functional Interface中的abstract方法上定義。這是一個(gè)讓人無(wú)法理解的規(guī)定。
遇到lambda的另一個(gè)坑:

由于username有重新賦值,所以編譯報(bào)錯(cuò),是不是很喜感?我不得不用一個(gè)臨時(shí)變量解決。。
官方提供了一種集成外部用戶(hù)體系的方法(Custom Database Integration Guide),然后并不支持加鹽密碼,于是我只能自己擼碼解決。關(guān)鍵是實(shí)現(xiàn)兩個(gè)接口:AuthProvider 和 UserProvider,只要實(shí)現(xiàn)部分方法即可,很簡(jiǎn)單不贅述。
部署
部署到centos7。首先 rpm -qa | grep openjdk 查看所有已安裝的jdk,如果版本不滿(mǎn)足則先 rpm -e --nodeps [java-1.7.0-openjdk[-headless]] 卸載掉。然后去官網(wǎng)上下載合適版本的server jre/jre/sdk包(下面會(huì)進(jìn)一步說(shuō)明),然后解壓,設(shè)置環(huán)境變量,就算安裝完畢了(不過(guò)這種安裝方式通過(guò)rpm -qa可是找不到的哦)。具體可看 Centos7 JDK8安裝配置。
講道理,jdk是開(kāi)發(fā)時(shí)候用的,部署的話(huà)我們只要安裝jre就可以了。我剛開(kāi)始下載的是server jre包,在ant的時(shí)候報(bào) package javafx.util does not exist 的錯(cuò)(因?yàn)槲以诖a里用到了Pair<>二元組,屬于javafx.util包),然而網(wǎng)上查了下,貌似javaFX是用于客戶(hù)端GUI方面的組件(不知道是否我這里報(bào)錯(cuò)的javafx同個(gè)概念)。我懶得探究,馬上去官網(wǎng)下了jre包(官網(wǎng)說(shuō)Covers most end-users needs. Contains everything required to run Java applications on your system.),載下來(lái)之后發(fā)現(xiàn)果然有jfxrt.jar(包含javafx.util),歡欣鼓舞,但是ant之后報(bào)無(wú)法找到/lib/tools.jar——因?yàn)閎uild.xml里有用到這個(gè)jar——之前server jre是有的,也是日了狗了。馬上去下jdk,瘋狂操作之后終于編譯通過(guò)。
也可以在windows平臺(tái)編譯打包,然后拷貝到linux系統(tǒng)。
官網(wǎng)上是說(shuō)./openfire start啟動(dòng)openfire,然而我只找到openfire.bat和openfirectl,先試了./openfirectl start 報(bào)錯(cuò):Could not find Openfire installation under /opt,/usr/share,or /usr/local,查看openfirectl的shell代碼,發(fā)現(xiàn)當(dāng)OPENFIRE_HOME未設(shè)置時(shí),會(huì)去這三個(gè)目錄下找openfire,于是為其設(shè)置真實(shí)根目錄,然而雖沒(méi)報(bào)錯(cuò),但還是沒(méi)有運(yùn)行起來(lái)。試了下openfire.bat,報(bào)Permission denied,尼瑪,我可是用root登錄的。先不管原因,我再去官網(wǎng)下了4.1.6(目前最新版)的tar包,發(fā)現(xiàn)bin目錄下果然有個(gè)openfire文件,拷到服務(wù)器上后報(bào)同樣的Permission denied的錯(cuò)誤——網(wǎng)上說(shuō)root并不默認(rèn)就有所有文件的最高權(quán)限,但是他可以隨意給自己增加權(quán)限——好吧,設(shè)置了權(quán)限之后,執(zhí)行./openfire start 沒(méi)報(bào)錯(cuò),但是依舊沒(méi)有運(yùn)行起來(lái)。。。后來(lái)發(fā)現(xiàn)沒(méi)有輸出錯(cuò)誤信息,是因?yàn)閟hell里寫(xiě)了/dev/null 2>&1,去掉之后終于提示——Could not find or load main class com.install4j.runtime.launcher.UnixLauncher——shell代碼里該類(lèi)指向的目錄本地編譯不存在,最后在官網(wǎng)tar包里發(fā)現(xiàn)有一個(gè)名為.install4j的隱藏文件夾,拷貝后總算運(yùn)行起來(lái)了。
記得打開(kāi)相應(yīng)端口。
webchat
用戶(hù)一般都是通過(guò)瀏覽器進(jìn)行咨詢(xún),有個(gè)webchat示例可以參考(openfire4.2 配置fastpath、webchat、spark實(shí)現(xiàn)客服系統(tǒng)),但那是基于很久以前的smack版本,轉(zhuǎn)過(guò)來(lái)也費(fèi)了不少勁,特別是QueueUpdate包擴(kuò)展已經(jīng)不再內(nèi)置支持,調(diào)試了半天在smack中找到幾個(gè)關(guān)鍵文件
,這些都是內(nèi)置資源文件,項(xiàng)目運(yùn)行時(shí)會(huì)讀取這些文件,調(diào)用ProviderManager.addExtensionProvider將配置項(xiàng)緩存起來(lái),如果不修改xml的話(huà),那么在外部調(diào)用該方法也是可以的。參照著寫(xiě)了一個(gè)QueueUpdateProvider,順便了解了下XmlPullParser的用法。
關(guān)于自定義包和擴(kuò)展,后來(lái)才發(fā)現(xiàn)官網(wǎng)上有介紹: Provider Architecture: Stanza Extensions and Custom IQ's,也是心累。
再后來(lái),發(fā)現(xiàn)部分非內(nèi)置的擴(kuò)展的Provider已經(jīng)在擴(kuò)展類(lèi)里[作為內(nèi)部類(lèi)]定義好了,比如QueueUpdate.Provider。。。吐血。關(guān)于內(nèi)部類(lèi)可參看 java中的內(nèi)部類(lèi)總結(jié)
部署
在CentOS安裝tomcat9.0.1。去官網(wǎng)下載tar.gz包,解壓,然后去到bin目錄,在catalina.sh文件添加內(nèi)容export CLASSPATH=$JAVA_HOME/lib,然后./startup.sh即可,另外記得開(kāi)放8080端口。當(dāng)然我們可以更改端口以及綁定域名,參考 tomcat發(fā)布應(yīng)用并配置域名。關(guān)于項(xiàng)目打包成war包,參考 Intellij IDEA社區(qū)版打包Maven項(xiàng)目成war包,并部署到tomcat上。
fastpath
增加幾個(gè)http接口,如新增客服組,添加客服等,示例代碼如下:
public class MasonServlet extends HttpServlet { @Override public void init(ServletConfig config) throws ServletException { super.init(config); AuthCheckFilter.addExclude("fastpath/mason/*"); // 公共接口不需身份校驗(yàn) } @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getRequestURI(); action = action.substring(action.indexOf("mason/") + 6); OPResult result = null; if (action.toLowerCase().equals("createworkgroup")) { String wgName = request.getParameter("wgName"); String description = request.getParameter("description"); String agents = request.getParameter("agents"); result = createWorkgroup(wgName, description, agents); } if (result == null) { result = new OPResult(); result.setSuccess(false); result.setMessage("未找到對(duì)應(yīng)方法"); } response.setContentType("application/json; charset=utf-8"); response.setCharacterEncoding("UTF-8"); Genson genson = new Genson(); String json = genson.serialize(result); response.getOutputStream().write(json.getBytes("UTF-8")); } // 新增工作組(會(huì)同時(shí)建立一個(gè)默認(rèn)客服組,每個(gè)工作組可以包含多個(gè)客服組) private OPResult createWorkgroup(String wgName, String description, String agents) { OPResult result = new OPResult(); Map errors = WorkgroupUtils.createWorkgroup(wgName, description, agents); if (errors.size() == 0) { Workgroup workgroup = WorkgroupManager.getInstance().getWorkgroup(wgName); result.setData(workgroup.getJID()); result.setSuccess(true); } else result.setSuccess(false); return result; } }
完了我們就可以重新構(gòu)建該插件了,在intellij中可以在窗口中設(shè)置(看了下build.xml,發(fā)現(xiàn)plugin任務(wù)可以構(gòu)建單個(gè)插件,它接收plugin的參數(shù)表明構(gòu)建的是哪個(gè)插件):

由于代碼中用到了genson這個(gè)第三方j(luò)ar包,雖然直接編譯沒(méi)問(wèn)題(項(xiàng)目的其它地方有引用),但用ant構(gòu)建的時(shí)候會(huì)報(bào)錯(cuò),提示找不到這個(gè)組件,原因官網(wǎng)說(shuō)了:Any JAR files your plugin needs during compilation should be put into the lib directory,因此我們需要將該jar包復(fù)制一份到fastpath/lib目錄下。
spark
此spark非彼spark,而是一個(gè)開(kāi)源IM桌面客戶(hù)端。下載下來(lái)2.8.3代碼,導(dǎo)入到IntelliJ,運(yùn)行輸出了空指針異常,調(diào)試發(fā)現(xiàn)找不到資源文件 "META-INF/plugins.xml",查看編譯后的jar文件,里面已經(jīng)包含了resources/META-INF/plugins.xml。再查看Project Structure,發(fā)現(xiàn)沒(méi)有為主模塊Spark設(shè)置Resource Folders,添加了resources文件夾后編譯運(yùn)行正常,此時(shí)再看jar文件,里面并沒(méi)有resources目錄,META-INF直接在根目錄體現(xiàn)。

也就是說(shuō),將某個(gè)目錄設(shè)置為資源文件夾(Resource Folders),意即將該目錄下的子目錄一起打包進(jìn)jar包(不包含該目錄本身),而getResource()方法獲取特定路徑的資源時(shí),是直接去jar包根目錄下查找對(duì)應(yīng)文件。
似乎還要設(shè)置VM arguments:-Djava.library.path=build/lib/dist/windows64,具體值按照操作系統(tǒng)來(lái)。參看 openfire-spark 二次開(kāi)發(fā)-(二)運(yùn)行環(huán)境配置
相關(guān)資料:TCP長(zhǎng)連接與短連接、心跳機(jī)制
轉(zhuǎn)載請(qǐng)注明本文出處:http://www.rzrgm.cn/newton/p/7269373.html

浙公網(wǎng)安備 33010602011771號(hào)