假如网站须要提供客服功能,若是只是简单的聊天咨询能够考虑营销QQ、百度商桥等(目前大部分网站采用此方式,包括一些知名行业电商);若是须要更精细化的管理,好比客服人员安排、各项数据统计汇总,那么须要对接专业的第三方客服平台,好比网易七鱼,固然价格不菲;然而如果如京东自己就是一个平台,须要为每一个商家提供各自的客服管理,首先目前第三方提供商并没有此类产品(网易七鱼听说已经开发出来了,可是官网上没找到),其次即便有,价格也确定不便宜,并且数据在别人那里总归很差。因此电商平台的客服系统,通常都是本身开发。固然了,借助优秀的开源项目,自主开发[一套简单能用的]也变得轻松不少。html
我采用了openfire+spark+layim,前二者基于java平台,layim是国人开发的一个webim前端组件。前端
先来看大体效果(左边是浏览器layim-客户提咨询,右边是spark聊天窗口-客服解答)java
图示:node
本文涉及到的知识点(杂乱,后续会不按期添加内容):linux
Java基础android
Intellij Idea:Java IDEweb
Mybatis:半ORMajax
XMPP协议spring
smack:XMPP协议的Java封装sql
openfire
fastpath:openfire插件,咱们须要依赖它实现客服功能
spark
一秒钟入门Java
Java SE(J2SE):Standard Edition,可认为是基础库,用于开发和部署桌面、服务器以及嵌入设备(J2ME)和实时环境中的Java应用程序。
Java EE(J2EE):基于SE的高级库,提供 Web 服务、组件模型、管理和通讯 API,能够用来实现企业级的面向服务体系结构。
能够知道J2EE比J2SE多了Web相关的组件和API,可是本人在使用SpringMVC框架开发Web应用程序时,去官网Java SE页面下载的JDK,也能正常开发。后来查看官网的Java EE的下载页面,发现提供的SDK中主要包含一个叫GlassFish的开源组件和一些示例及文档,而Java EE刚开始是以一种规范提出,GlassFish能够看做是实现了这些规范的JEE容器,而咱们开发Web站点时部署到服务器(好比Tomcat),实现了JEE规范其中的Servlet容器部分,因此以JDK开发Web并不会出现问题。
JNDI 是什么 :简单的说就是为了解耦,非直接引用,而是经过名称或地址查找而后加载的方式,经常使用依赖注入的方式实现。
目前流行的IDE有Eclipse和IntelliJ IDEA,前者免费且因为历史关系占有率一直很高,后者也有社区版,听说使用性上目前完胜前者。
final关键词:相似于.NET的readonly
匿名内部类:
定义一个类A(能够为abstract),为方便说明,在A中定义一个[抽象]方法dosth。在调用方法里能够直接new A,而且同时给dosth赋方法体。
public abstract A{ public void dosth() { } } public abstract B{ public void call() { final A a = new A() { public void dosth() { //这里写方法体 } }; } }
看着是实例化了A的一个对象,实际上是实例化了A类的匿名子类。
Access restriction:eclipse对某些java包(or 类?)有access rules,好比 sun.awt.shell.ShellFolder。由于这些JAR默认包含了一系列的代码访问规则(Access Rules),若是代码中引用了这些访问规则所禁止引用类,那么就会提示这个错误信息。解决方法:既然存在访问规则,那么修改访问规则便可。打开项目的Build Path Configuration页面,打开引用的[报错]JAR包,选中Access rules条目,选择右侧的编辑按钮,添加一个访问规则便可。
Java NIO
Apache Mina
CopyOnWrite:CopyOnWrite容器即写时复制的容器。通俗的理解是当咱们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,而后新的容器里添加元素,添加完元素以后,再将原容器的引用指向新的容器。这样作的好处是咱们能够对CopyOnWrite容器进行并发的读,而不须要加锁。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。
Maven:项目管理工具。不像VS,eclipse是更纯粹的编码工具,在维护jar包和项目之间的依赖关系、项目的构建目标等方面的功能比较弱(好比拷贝了一个项目,咱们须要手动去Configure Build Path),而Maven就是补足于此。Maven独立于IDE,eclipse有一个插件叫M2E,里面内置了Maven。Maven项目的配置信息保存在pom.xml文件中。
咱们在导入Maven项目时,有时会发现不止一个pom.xml,那是由于项目中有子项目(module,module有本身的pom.xml),只要选择最顶层的pom.xml文件便可,会自动加载引用到的子项目。
JavaBean:通常可看做是POJO,可参看 Java Bean 是个什么概念? (不过这个问题里有个答主说Java没有事件的概念,让我大吃一惊,不过转念一想,Java主要用于开发服务端应用,确实不怎么涉及到[自定义]事件。其实Java中是有事件机制的,只是不知变通,就一个半成品的观察者模式,想一想C#的委托,其实就一个函数指针的事)
MVC:当.Neter们在被Asp.Net的重量压得踹不过气来的时候,Java已经有MVC的概念了。不少模式,.Net界都是直接copy,.Neter们并无对其历史的认知,因此接收不能,MVC就是如此。其实在Asp.Net时代已经有MVC的影子,就是通常处理程序.ashx。很早之前,用户提交都是提交到具体的一个页面,因而会常常致使一个页面并非用于显示,而是用于业务逻辑的处理,因而后来把业务逻辑单独拎出来,这即是Controller,用户请求的是Controller,再也不是具体页面,而且Controller里再也不使用相似HttpRequest或者HttpResponse获取数据和返回响应,而是使用对象的形式(M),这即是MVC模式。可参看 Java Web开发模式
Java中的注解至关于.NET中的Attribute。
Spring是一个IOC和AOP框架。咱们能够经过在xml文件中配置bean,而后在代码中使用@Autowired或@Resource注入bean实例,不过配置的环节稍显繁琐,能不能省略呢?答案是确定的,Sping2.5开始支持注解注入,具体可看 spring注解注入:<context:component-scan>详解。须要注意的是,@Component及相关的几个注解类,在应用到interface上的时候,可能并不如预期工做,由于interface并不能实例化,而这几个注解类貌似又没有@Inherited修饰,因此就算有实现类或运行时的动态实现类,也不会注册到上下文中;且修饰的类要有公共构造函数。另外注入[被注入方]通常只能在注入方自己是已注册的bean里,若在普通类里想经过@Autowired或@Resource的方式注入bean,则稍微有点绕,可参看 Java普通类获取Spring XML中Bean的方法总结
关于Servlet、Struts、Spring、SpringMVC的关系与区别可参看 Java开发web的几种开发模式 和 SpringMVC与Struts2的对比
SpringMVC居然URL和参数大小写敏感,虽然有办法配置,但这种预设没有道理吧。。。
Servlet url-pattern /与/*区别:二者的长度不一样,根据最长路径匹配的优先级,/*比/更容易被选中,而/的真正含义是,缺省匹配。既全部的URL都没法被选中的时候,就必定会选中/,可见它的优先级是最低的,这就二者的区别。
xml文件也能够打包进jar包,可是访问jar包里的xml文件就不能按文件目录的方式来了,可参看 http://blog.csdn.net/jianxin1009/article/details/18814799
application.getInitParameter:jsp中9个内置对象之一application,它的数据对整个web应用都有效,application有一个重要的用途就是经过getInitParameter()获取web.xm中的配置参数,这样能够提升代码的移植性。
dwr:简化ajax调用,使得调用远程服务器方法看上去像调用本地方法同样。
在java项目中必不可少的是咱们要指定一个jdk。在指定jdk的同时,还能够指定jdk的Language level,这个有点像咱们工程最低支持版本。好比Language level 设置了5.0 只是就不能出现使用6.0/7.0特性的代码,由于这些特性在5.0的环境下是没法编译的。或者能够理解ide会安装Language level指定的jdk版原本对咱们的代码进行编译,以及错误检查。即一样的jdk对应不一样的Language Level会采用[可能]不一样的编译和优化方式。
Java中也有相似.Net的字符串池的概念,请看 String中intern的方法
Java插件技术: OSGi
貌似在同一package下,protected可见。(和.NET不一样)
Java的泛型类型只能是引用类型,而不能是基础类型,可是Java针对每一个基础类型有对应的封装类型,好比boolean对应Boolean,后者是引用类型,能够为null,当封装类型不为null时,能够隐式转换,但写代码时null的状况要本身处理,如
private boolean existUser(String username) { Boolean result = null; return result != null && result.booleanValue(); }
Ant:相似于.NET的MSBuild,其构建文件默认为build.xml(能够在其中指定构建基于的Java平台版本),每一个构建文件都对应于一个项目,可是大型项目常常包含大量的子项目,每个子项目均可以有本身的构建文件。
一个.java文件中能够定义多个类,可是public修饰的只能至多有一个,且要与文件名相同,编译后,有几个类就会产生几个对应的.class文件。jar包相似.Net的dll,它将多个.class文件打包一块。大多数 JAR 文件包含一个 META-INF 目录,它用于存储包和扩展的配置数据,如安全性和版本信息。Java 2 平台识别并解释 META-INF 目录中的下述文件和目录,以便配置应用程序、扩展和类装载器。具体可看 MANIFEST.MF 文件内容彻底详解。
System.getProperty()获取系统/项目全局变量,好比Java运行时版本,固然咱们也能够经过System.setProperty()设置自定义变量。
Java桌面客户端编程:Java Swing 。桌面程序毕竟不是Java的主流领域,所以各IDE貌似也并未做太多努力,相较VS的所见即所得的控件拖拽开发模式,Java GUI编程就吃力不少了。
Java国际化:i18n,注意中文的资源文件,貌似须要先UTF-8转码,大约就是像这样。(可使用JDK自带的native2ascii.exe)
Intellij Idea
使用Intellij Idea建立spring mvc时(没用maven),run都报 Error during artifact deployment. See server log for details 错误,后来把lib文件夹拷到WEB-INFO文件夹下就没问题了,不知何故。
缘由:tomcat默认是去web-info/lib/下找依赖的jar包。手动拷jar包毕竟不是一个好办法,其实咱们能够在下图处进行Artifacts设置
运行项目,项目目录下会多出一个out文件夹,生成全部的站点文件,依赖包会自动拷贝到下面的WEB-INF/lib/下,以下图:
IDEA配置artifacts中Web Application:Exploded和Web Application:Archive的区别:前者以文件夹形式(War Exploded)发布项目,后者以war包形式(每次都会从新打包所有的)。Tomcat会自动解压war包并启动站点,缺点是会形成一段时间的站点不可用,而以文件夹形式发布的话,则支持热部署(需进行额外的一些配置)。
固然咱们也可使用Maven进行依赖包的管理。在当前项目右键->Add Framework Support->Maven便可。注意须要在Project Structure-> Project Settings中移除以前非Maven引用的包依赖。此时运行项目,项目目录下会多出一个target文件夹,其下有生成的站点文件。可是运行时发现WEB-INF下的文件除了web.xml外,其它的文件都不会覆盖,貌似用maven管理的web工程,须要将applicationContext.xml等资源文件放在resource目录下,而后以classpath的方式去访问。后来发现jsp页面也没法自动更新到target目录,再后来据说maven有一套约定的目录结构,貌似又能够经过pom.xml进行自定义配置,神烦!目前靠手动覆盖。参考 Maven使用点滴 配置便可(webappDirectory我没设置,就设置了warSourceDirectory,能正常更新了)
Intellij Idea中有个Ant Build Window,默认显示的是主项目下build.xml中的targets,and by default, IDEA only shows the default target and targets that have descriptions。对这个有疑问可参看 How to get Ant Build to list all targets in a hierarchy of build files.
能够在Run/Debug Configurations Window中设置自定义系统变量,以下图(-D不能省):
MyBatis
一个半ORM框架,SQL语句并非像EF同样由框架解析,而是要预先写在xml中或者写在Java注解(同.Net的Attribute)中,且不支持匿名类型(即select出来的数据要么是基础类型,要么要有对应的Java Bean)。通常状况下,咱们使用resultType映射查询结果和对象便可(MyBatis 会在幕后自动建立一个 ResultMap),当只想映射部分字段或者包含复杂类型属性的时候,咱们须要自定义ResultMap。
MyBatis不支持方法重载,由于它是经过方法名称(不加参数)去查找执行方法,所以咱们设置不一样的方法名,或者使用动态sql。
XMPP协议
JID表示一个地址,由三部分组成——node、domain和resource。例如:xiaoming@xiaoming.home/sleeping,xiaoming就是node ,xiaoming.home就是domain,sleeping就是resource。node domain 和resource任何一部分都不能超过1023 字节 ,加上@和 /,一个JID 总共不能超过3071字节。BareJid就是去掉resource,只包含node@domain。
XMPP包含IQ, message and presence 三种packet。
smack
ConnectionConfiguration.Builder的setXmppDomain和setHost的区别?一个是域(服务器集群),一个是其中的一台服务器,应该只要设置其中一个就能够了。
使用XMPPTCPConnectionConfiguration创建链接时报空指针错误,调试发现有个base64encoder未赋值,须要引用smack-java7包,该包会初始化base64encoder,若是是安卓开发,那么就引用smack-android。
openfire
使用idea导入openfire代码,过程可参考将openfire源码部署到IDEA中 或者 IntelliJ IDEA搭建openfire4.1.3开发环境 。使用openfire配置界面只能配置一个数据库,且我也不打算彻底依赖它生成的数据库。我须要openfire部分功能使用现有的数据库(好比用户表),而openfire的业务数据仍然使用生成的数据库,所以涉及到多库链接。这只能去修改源码了。
上面说到的配置界面设置的项最终存储在ofproperty表中。在配置界面完成配置后,咱们也能够在conf/openfire.xml中从新设置值,重启openfire,配置文件中的值会更新到数据库中。
以AuthFactory为例,其initProvider方法里有 JiveGlobals.migrateProperty("provider.auth.className"); ,XMLProperties根据"provider.auth.className"读取xml文件中的值(getProperty方法)
//按逗号拆分为数组 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();
对应的配置节写法以下(能够看到,propName对应各层级element,而非attribute形式)
<provider> <auth> <className>org.jivesoftware.openfire.auth.JDBCAuthProvider</className> </auth> </provider>
然后覆盖数据库值
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."); } } }
固然,如果咱们有数据库权限,直接进入数据库修改也同样。
openfire源码采用JDBC方式操做数据库,并且没有作很好的封装,重复代码较多,以下图所示
类似代码在与数据库交互的地方随处可见。部分逻辑的抽取,莫过于lambda(回调函数)的方式。考虑到Java8已经支持lambda表达式,重构以下:
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; }
可是在写调用代码的时候提示:
虽然咱们在excuteQuery方法中已经catch了这个异常,可是编译器并不买帐。并且就算咱们在方法定义时已经throws了相关异常,也没用,以下图:
解决方法有两种:能够在lambda体内catch异常后再也不throw;或者自定义一个Functional Interface,其中声明一个定义了异常的方法,
@FunctionalInterface public interface CheckedSQLExceptionFunction<T, R> { R apply(T t) throws SQLException; }
而后将Function<Result,T>的地方替换为CheckedSQLExceptionFunction<ResultSet, T>。这两种都显得别扭与不合理,致使这一问题的是,Java Lambda规定若是Lambda中抛出了异常,那么这个异常必定要在Functional Interface中的abstract方法上定义。这是一个让人没法理解的规定。
遇到lambda的另外一个坑:
因为username有从新赋值,因此编译报错,是否是很喜感?我不得不用一个临时变量解决。。
官方提供了一种集成外部用户体系的方法(Custom Database Integration Guide),而后并不支持加盐密码,因而我只能本身撸码解决。关键是实现两个接口:AuthProvider 和 UserProvider,只要实现部分方法便可,很简单不赘述。
部署
部署到centos7。首先 rpm -qa | grep openjdk 查看全部已安装的jdk,若是版本不知足则先 rpm -e --nodeps [java-1.7.0-openjdk[-headless]] 卸载掉。而后去官网上下载合适版本的server jre/jre/sdk包(下面会进一步说明),而后解压,设置环境变量,就算安装完毕了(不过这种安装方式经过rpm -qa但是找不到的哦)。具体可看 Centos7 JDK8安装配置。
讲道理,jdk是开发时候用的,部署的话咱们只要安装jre就能够了。我刚开始下载的是server jre包,在ant的时候报 package javafx.util does not exist 的错(由于我在代码里用到了Pair<>二元组,属于javafx.util包),然而网上查了下,貌似javaFX是用于客户端GUI方面的组件(不知道是否我这里报错的javafx同个概念)。我懒得探究,立刻去官网下了jre包(官网说Covers most end-users needs. Contains everything required to run Java applications on your system.),载下来以后发现果真有jfxrt.jar(包含javafx.util),欢欣鼓舞,可是ant以后报没法找到/lib/tools.jar——由于build.xml里有用到这个jar——以前server jre是有的,也是日了狗了。立刻去下jdk,疯狂操做以后终于编译经过。
也能够在windows平台编译打包,而后拷贝到linux系统。
官网上是说./openfire start启动openfire,然而我只找到openfire.bat和openfirectl,先试了./openfirectl start 报错:Could not find Openfire installation under /opt,/usr/share,or /usr/local,查看openfirectl的shell代码,发现当OPENFIRE_HOME未设置时,会去这三个目录下找openfire,因而为其设置真实根目录,然而虽没报错,但仍是没有运行起来。试了下openfire.bat,报Permission denied,尼玛,我但是用root登陆的。先无论缘由,我再去官网下了4.1.6(目前最新版)的tar包,发现bin目录下果真有个openfire文件,拷到服务器上后报一样的Permission denied的错误——网上说root并不默认就有全部文件的最高权限,可是他能够随意给本身增长权限——好吧,设置了权限以后,执行./openfire start 没报错,可是依旧没有运行起来。。。后来发现没有输出错误信息,是由于shell里写了/dev/null 2>&1,去掉以后终于提示——Could not find or load main class com.install4j.runtime.launcher.UnixLauncher——shell代码里该类指向的目录本地编译不存在,最后在官网tar包里发现有一个名为.install4j的隐藏文件夹,拷贝后总算运行起来了。
记得打开相应端口。
webchat
用户通常都是经过浏览器进行咨询,有个webchat示例能够参考(openfire4.2 配置fastpath、webchat、spark实现客服系统),但那是基于好久之前的smack版本,转过来也费了很多劲,特别是QueueUpdate包扩展已经再也不内置支持,调试了半天在smack中找到几个关键文件,这些都是内置资源文件,项目运行时会读取这些文件,调用ProviderManager.addExtensionProvider将配置项缓存起来,若是不修改xml的话,那么在外部调用该方法也是能够的。参照着写了一个QueueUpdateProvider,顺便了解了下XmlPullParser的用法。
关于自定义包和扩展,后来才发现官网上有介绍: Provider Architecture: Stanza Extensions and Custom IQ's,也是心累。
再后来,发现部分非内置的扩展的Provider已经在扩展类里[做为内部类]定义好了,好比QueueUpdate.Provider。。。吐血。关于内部类可参看 java中的内部类总结
部署
在CentOS安装tomcat9.0.1。去官网下载tar.gz包,解压,而后去到bin目录,在catalina.sh文件添加内容export CLASSPATH=$JAVA_HOME/lib,而后./startup.sh便可,另外记得开放8080端口。固然咱们能够更改端口以及绑定域名,参考 tomcat发布应用并配置域名。关于项目打包成war包,参考 Intellij IDEA社区版打包Maven项目成war包,并部署到tomcat上。
fastpath
增长几个http接口,如新增客服组,添加客服等,示例代码以下:
public class MasonServlet extends HttpServlet { @Override public void init(ServletConfig config) throws ServletException { super.init(config); AuthCheckFilter.addExclude("fastpath/mason/*"); // 公共接口不需身份校验 } @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("未找到对应方法"); } 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")); } // 新增工做组(会同时创建一个默认客服组,每一个工做组能够包含多个客服组) 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; } }
完了咱们就能够从新构建该插件了,在intellij中能够在窗口中设置(看了下build.xml,发现plugin任务能够构建单个插件,它接收plugin的参数代表构建的是哪一个插件):
因为代码中用到了genson这个第三方jar包,虽然直接编译没问题(项目的其它地方有引用),但用ant构建的时候会报错,提示找不到这个组件,缘由官网说了:Any JAR files your plugin needs during compilation should be put into the lib directory,所以咱们须要将该jar包复制一份到fastpath/lib目录下。
spark
此spark非彼spark,而是一个开源IM桌面客户端。下载下来2.8.3代码,导入到IntelliJ,运行输出了空指针异常,调试发现找不到资源文件 "META-INF/plugins.xml",查看编译后的jar文件,里面已经包含了resources/META-INF/plugins.xml。再查看Project Structure,发现没有为主模块Spark设置Resource Folders,添加了resources文件夹后编译运行正常,此时再看jar文件,里面并无resources目录,META-INF直接在根目录体现。
也就是说,将某个目录设置为资源文件夹(Resource Folders),意即将该目录下的子目录一块儿打包进jar包(不包含该目录自己),而getResource()方法获取特定路径的资源时,是直接去jar包根目录下查找对应文件。
彷佛还要设置VM arguments:-Djava.library.path=build/lib/dist/windows64,具体值按照操做系统来。参看 openfire-spark 二次开发-(二)运行环境配置
相关资料:TCP长链接与短链接、心跳机制