Ubuntu 22.04使用Podman部署Tomcat 9的详细教程

安装必要的依赖:

# 安装 podman
$ sudo apt install podman

# 准备本地目录映射
$ mkdir /home/data/.tomcat

$ mkdir /home/data/.tomcat/webapps

$ mkdir /home/data/.tomcat/logs

官方镜像会在报错的时候暴露 Tomcat 9 版本号,错误堆栈,构成安全隐患,我们需要通过构建自定义镜像解决此问题:

$ touch Dockerfile

内容如下:

# 详细启动日志在 /usr/local/tomcat/logs 目录下

From docker.io/library/tomcat:9

# xml 编辑工具,方便我们后续修改 tomcat 的xml配置文件
# RUN apt update && apt install -y xmlstarlet

# 增加 <Valve className="org.apache.catalina.valves.ErrorReportValve" showReport="false" showServerInfo="false" /> 阻止错误日志输出

RUN sed -i "s/<\/Host>/  <Valve className='org.apache.catalina.valves.ErrorReportValve' showReport='false' showServerInfo='false' \/>\n\t<\/Host>/" /usr/local/tomcat/conf/server.xml

# 配置AJP协议
# 如果只希望通过 AJP访问,可以参考如下命令 移除原有的 AJP 协议配置
# RUN xmlstarlet ed -L -P -S -d '/Server/Service/Connector' /usr/local/tomcat/conf/server.xml

# 增加新的协议配置
RUN sed -i "s/<\/Service>/  <Connector port='8009' protocol='AJP\/1.3' address='0.0.0.0' redirectPort='8443' secretRequired=''\/>\n  <\/Service>/" /usr/local/tomcat/conf/server.xml

# 移除 xml 编辑工具
# RUN apt -y autoremove --purge xmlstarlet

构建镜像:

$ sudo podman build -t tomcat-9 .

设置容器开机自启:

$ sudo podman stop tomcat-9

$ sudo podman rm tomcat-9

# 启动一个 tomcat 容器 8080 HTTP 访问端口 8009 AJP访问端口
# sudo podman run -d --name tomcat-9 -p 8080:8080 -p 8009:8009 -v /my/local/path:/usr/local/tomcat/webapps localhost/tomcat-9

$ sudo podman run -d --name tomcat-9 -p 8080:8080 -p 8009:8009 -v /home/data/.tomcat/webapps:/usr/local/tomcat/webapps -v /home/data/.tomcat/logs:/usr/local/tomcat/logs localhost/tomcat-9

# 查看该容器
$ sudo podman ps

# 如果需要进入容器查看执行情况,参考如下命令
# sudo podman exec -it tomcat-9 bash

# 每次都启动新容器方式创建servcie //--new参数,每次启动都删除旧容器,启动一个新容器 
$ sudo podman generate systemd --restart-policy=always -n --new -f tomcat-9

查看启动文件:

$ cat container-tomcat-9.service

内容如下:

# container-tomcat-9.service
# autogenerated by Podman 3.4.4
# Sun Mar 10 12:31:31 CST 2024

[Unit]
Description=Podman container-tomcat-9.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStopSec=70
ExecStartPre=/bin/rm -f %t/%n.ctr-id
ExecStart=/usr/bin/podman run --cidfile=%t/%n.ctr-id --cgroups=no-conmon --rm --sdnotify=conmon --replace -d --name tomcat-9 -p 8080:8080 -p 8009:8009 -v /home/data/.tomcat/webapps:/usr/local/tomcat/webapps -v /home/data/.tomcat/logs:/usr/local/tomcat/logs localhost/tomcat-9
ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all

[Install]
WantedBy=default.target

Systemd 配置:

# 保存到/etc/systemd/system/
$ sudo mv container-tomcat-9.service /etc/systemd/system/

# 刷新配置文件,让其生效
$ sudo systemctl daemon-reload

# 设置容器开机自启,并且现在启动
$ sudo systemctl enable --now /etc/systemd/system/container-tomcat-9.service

# 如果需要进入容器查看执行情况,参考如下命令
# sudo podman exec -it tomcat-9 bash

# 测试,重启虚拟机
$ sudo reboot

# 启动或重启服务 
# systemctl --user start container-tomcat-9.service 
# systemctl --user restart container-tomcat-9.service

# 如果启动失败,观察服务日志
# sudo journalctl -f

后续 WAR 包存储到 /home/data/.tomcat/webapps 目录下即可进行正常访问。

参考链接


Tomcat 10运行项目出现java.lang.NoClassDefFoundError: javax/servlet/ServletContextListener的问题

Tomcat 9更新到Tomcat 10,运行项目,发现报错:

java.lang.NoClassDefFoundError: javax/servlet/ServletContextListener
        at java.base/java.lang.ClassLoader.defineClass1(Native Method)
        at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1017)
        at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:151)
        at org.apache.catalina.loader.WebappClassLoaderBase.findClassInternal(WebappClassLoaderBase.java:2470)
        at org.apache.catalina.loader.WebappClassLoaderBase.findClass(WebappClassLoaderBase.java:866)
        at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1370)
        at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1224)
        at org.apache.catalina.core.DefaultInstanceManager.loadClass(DefaultInstanceManager.java:540)
        at org.apache.catalina.core.DefaultInstanceManager.loadClassMaybePrivileged(DefaultInstanceManager.java:521)
        at org.apache.catalina.core.DefaultInstanceManager.newInstance(DefaultInstanceManager.java:151)
        at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4589)
        at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5121)
        at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
        at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:717)
        at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:690)
        at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:743)
        at org.apache.catalina.startup.HostConfig.manageApp(HostConfig.java:1865)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:564)
        at org.apache.tomcat.util.modeler.BaseModelMBean.invoke(BaseModelMBean.java:288)
        at java.management/com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.invoke(DefaultMBeanServerInterceptor.java:809)
        at java.management/com.sun.jmx.mbeanserver.JmxMBeanServer.invoke(JmxMBeanServer.java:801)
        at org.apache.catalina.mbeans.MBeanFactory.createStandardContext(MBeanFactory.java:428)
        at org.apache.catalina.mbeans.MBeanFactory.createStandardContext(MBeanFactory.java:376)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:564)
        at org.apache.tomcat.util.modeler.BaseModelMBean.invoke(BaseModelMBean.java:288)
        at java.management/com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.invoke(DefaultMBeanServerInterceptor.java:809)
        at java.management/com.sun.jmx.mbeanserver.JmxMBeanServer.invoke(JmxMBeanServer.java:801)
        at java.management/com.sun.jmx.remote.security.MBeanServerAccessController.invoke(MBeanServerAccessController.java:468)
        at java.management.rmi/javax.management.remote.rmi.RMIConnectionImpl.doOperation(RMIConnectionImpl.java:1466)
        at java.management.rmi/javax.management.remote.rmi.RMIConnectionImpl$PrivilegedOperation.run(RMIConnectionImpl.java:1307)
        at java.base/java.security.AccessController.doPrivileged(AccessController.java:691)
        at java.management.rmi/javax.management.remote.rmi.RMIConnectionImpl.doPrivilegedOperation(RMIConnectionImpl.java:1406)
        at java.management.rmi/javax.management.remote.rmi.RMIConnectionImpl.invoke(RMIConnectionImpl.java:827)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:564)
        at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:359)
        at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
        at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
        at java.base/java.security.AccessController.doPrivileged(AccessController.java:691)
        at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
        at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:587)
        at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
        at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:705)
        at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
        at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:704)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
        at java.base/java.lang.Thread.run(Thread.java:832)
        Caused by: java.lang.ClassNotFoundException: javax.servlet.ServletContextListener
        at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1401)
        at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1224)
        ... 56 more

项目无法正常启动,看官方提示才知道,原来javax改成了jakarta了,所以找不到对应的包文件。我就不对项目进行修改,用回Tomcat 9就挺好的。

继续阅读Tomcat 10运行项目出现java.lang.NoClassDefFoundError: javax/servlet/ServletContextListener的问题

Jakarta EE - Java EE的终结者

什么是 Jakarta EE

Jakarta EE并不是新技术,他的前身就是大家熟悉的Java EE,老一辈的程序员可能还记得J2EE,是的,他们都是同一个东西,至于为什么会改来改去,这里面就有很多故事了。

1998年12月,SUN公司发布了JDK1.2,开始使用Java 2 这一名称,第二年Sun公司联合IBM、Oracle、BEA等大型企业应用系统开发商共同制订了一个基于Java组件技术的企业应用系统开发规范,名字很自然就取为Java 2 Platform Enterprise Edition简称J2EE,后面的事情大家也知道,JDK版本升的很快,J2EE名称如果跟着Java版本走必然会给开发人员造成困惑不利于该技术的推广,终于在2006年,SUN公司在发布Java 5后正式将J2EE改名为Java EE(Java Platform, Enterprise Edition),很多早期j2ee开发者虽然现在用的是最新的java ee标准但他们还是认为自己在用j2ee,当然,只是名称的改变并没有给开发者带来什么麻烦,相比之下下面这个就是要命。

2009年,Oracle宣布收购SUN,Java相关技术自然归Oracle所有,在2017年,Oracle 宣布开源 Java EE 并将项目移交给 Eclipse 基金会,但Oracle移交的很不痛快,提了很多要求,其中就包括不能再使用Java EE这个名称,虽然有点无理但Eclipse基金会还是接受了这个要求并且改名为Jakarta EE,经历了从j2ee到java ee的改名再经历一次似乎也无所谓,但要命的是Oracle要求Jakarta EE不能修改javax命名空间,这个就意味着java ee移交后代码就此封版,如果想修改代码,不好意思,请另起炉灶,所以,Oracle你移交的意义在哪?

那么从Java EE到Jakarta EE会给企业带来什么影响?下面我们一起分析。

名称的转变

这个对企业影响不大,对开发者影响也不大,你可以愉快用着Jakarta EE最新标准的同时跟同事说java ee发布新版本了。

命名空间的转变

如果你是用Maven进行开发,那么Java EE的依赖是这么定义的

<dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-api</artifactId>
    <version>8.0.1</version>
    <scope>provided</scope>
</dependency>

我们可以看到groupId是javax,并且源码的结构如下:

.
└── javax
    ├── annotation
    ├── batch
    ├── decorator
    ├── ejb
    ├── el
    ├── enterprise
    ├── faces
    ├── inject
    ├── interceptor
    ├── jms
    ├── json
    ├── mail
    ├── management
    ├── persistence
    ├── resource
    ├── security
    ├── servlet
    ├── transaction
    ├── validation
    ├── websocket
    ├── ws
    └── xml

所有的源码都定义在javax.*这个路径下,根据Oracle的要求Jakarta EE不能修改javax命名空间,那么Jakarta EE只能将代码中javax修改为jakarta,因此最新版本Jakarta Maven描述是长这样的:

<dependency>
    <groupId>jakarta.platform</groupId>
    <artifactId>jakarta.jakartaee-api</artifactId>
    <version>9.0.0-RC2</version>
    <scope>provided</scope>
</dependency>

源码目录:

.
└── jakarta
    ├── activation
    ├── annotation
    ├── batch
    ├── decorator
    ├── ejb
    ├── el
    ├── enterprise
    ├── faces
    ├── inject
    ├── interceptor
    ├── jms
    ├── json
    ├── jws
    ├── mail
    ├── persistence
    ├── resource
    ├── security
    ├── servlet
    ├── transaction
    ├── validation
    ├── websocket
    ├── ws
    └── xml

可以看到除了顶层包名称不一样下面的定义都一样,那么包路径更改会有什么影响?影响非常大。

  • 所有java ee应用服务器比如Weblogic,GlassFish等如果要支持最新版本的jakarta ee就必须修改源码重新编译,并且如果支持了jakarta ee就无法支持java ee,也就是无法向前兼容,Tomcat虽然只是Servlet容器但是Servlet本身就是Java EE的一部分,因此也逃不过修改的命运。据说Tomcat可以对应用进行自动代码转换以支持jakarta,因此在不远的将来我们可以看到各种奇技淫巧去兼容jakarta,是不是想起了被IE支配的恐惧。
  • 企业如果需要用到jakarta ee最新特性必须修改现有代码,修改并不复杂,就是把代码中import javax.*替换为import jakarta.*,修改完重新编译打包部署,似乎很简单,事实上没那么简单。
  • 对企业来说,保持应用服务器处于最新版本是必须的,因为新版本可能修复了老版本的漏洞,并且性能上也可能有一定的提升,但如果升级应用服务器的同时也要修改源码代码就很大,修改的成本,带来的风险并不是所有企业都能接受的。
  • 假设修改了一个应用,那么就需要部署到新版本应用服务器上,由于新服务器不兼容老应用因此需要运维两套应用服务器,运维成本提高,两套应用服务器也可能涉及license问题,不知道各厂商要怎么解决这个问题。

总之企业面临两难的境地,要么升级改系统源码,要么保持不变不升级,要么部分升级运维两套应用服务器,刀刀都要命。

未来

在如今各种诸如spring boot框架的包装下,在应用层面已经找不到Java EE的影子了,这些框架完全有能力抛弃jakarta自己实现,对于这些框架来说,跟随jakarta的意义似乎不大。对于企业来说,拥抱spring boot已经大势所趋,Java EE又搞出这么一件事,只会更加坚定企业转型的决心。对于开发者来说,Java EE已经是古董级的技术,Spring Boot不香吗,有什么理由去用jakarta EE。Oracle似乎也是看到了Java EE行将就木就索性移交出去,还能换取Eclipse基金会的董事会席位,但我还是看不懂Oracle对Java EE致命的这一刀目的何在,weblogic和中间件也是Oracle重要的两块业务,这两块业务都依赖Java EE,这么做只会两败俱伤。

参考


java.lang.IllegalArgumentException: The AJP Connector is configured with secretRequired="true" but the secret attribute is either null or "".

ubuntu 18.04 .5 升级到 ubuntu 20.04.2 之后,发现 Tomcat 9.0.31 长时间没办法启动,观察日志,发现如下错误信息:

08-Feb-2021 22:09:23.792 SEVERE [main] org.apache.catalina.util.LifecycleBase.handleSubClassException Failed to start component [Connector[AJP/1.3-8009]]
        org.apache.catalina.LifecycleException: Protocol handler start failed
                at org.apache.catalina.connector.Connector.startInternal(Connector.java:1038)
                at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
                at org.apache.catalina.core.StandardService.startInternal(StandardService.java:438)
                at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
                at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:930)
                at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
                at org.apache.catalina.startup.Catalina.start(Catalina.java:633)
                at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
                at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
                at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
                at java.base/java.lang.reflect.Method.invoke(Method.java:566)
                at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:343)
                at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:478)
        Caused by: java.lang.IllegalArgumentException: The AJP Connector is configured with secretRequired="true" but the secret attribute is either null or "". This combination is not valid.
                at org.apache.coyote.ajp.AbstractAjpProtocol.start(AbstractAjpProtocol.java:264)
                at org.apache.catalina.connector.Connector.startInternal(Connector.java:1035)
                ... 12 more

这个是由于在升级系统的时候,选择保留老版本的配置文件,这样就导致,如果 Tomcat 配置了通过 AJP 方式与Apache通信的情况下,会报告上面的错误信息。

新增 secretRequired 的目的是为了解决 AJP  端口暴露在公网的情况下,存在 AJP File Read/Inclusion in Apache Tomcat (CVE-2020-1938) and Undertow (CVE-2020-1745) 漏洞,由于 Tomcat AJP 协议设计上存在缺陷,攻击者通过 Tomcat AJP Connector 可以读取或包含 Tomcat 上所有 webapp 目录下的任意文件,例如可以读取 webapp 配置文件或源代码。此外在目标应用有文件上传功能的情况下,配合文件包含的利用还可以达到远程代码执行的危害。

连接方必须在连接的时候,传入正确的 secretRequired 才能与 Tomcat 通信,相当于通信需要的密码了。

网上很多人都是直接设置

<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" secretRequired="" />

来解决问题的,也就是设置 secretRequired 为空字符,但是这样会导致攻击方不需要传递密码就可以通信了,因此诱发远程攻击漏洞。

正确的做法其实是不允许远程用户直接通过 Tomcat AJP 协议通信,也就是在设置绑定的 IP 地址为本地地址 127.0.0.1。如下:

<Connector port="8009" protocol="AJP/1.3" address="127.0.0.1" redirectPort="8443" secretRequired=""/>

参考链接


Ubuntu Server 18.04 LTS隐藏Tomcat-9.0.16.0的版本号与操作系统类型

推荐方案

另外更推荐的方法是通过 Tomcat 的配置文件完成,而不是修改代码,具体配置方法为:

conf/server.xml 配置文件中的 <Host> 配置项中添加如下配置:

<Valve className="org.apache.catalina.valves.ErrorReportValve" showReport="false" showServerInfo="false" />

配置项说明:

  • showReport:默认值为true,默认显示报错信息
  • showServerInfo:默认值为true,默认显示Tomcat的版本号

其他方案

其他的方案跟  Ubuntu 14.04隐藏Tomcat-7.0.52的版本号与操作系统类型 是一致的,但是具体的细节上存在不小的差异,还是需要记录一下。

$ cd ~

$ mkdir catalina

$ cd catalina

$ cp /usr/share/tomcat9/lib/catalina.jar .

$ unzip catalina.jar

$ cd org/apache/catalina/util

$ vim ServerInfo.properties

可以看到里面的内容如下:

# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

server.info=Apache Tomcat/9.0.16 (Ubuntu)
server.number=9.0.16.0
server.built=Sep 11 2019 19:47:51 UTC

直接注释掉里面的内容,如下:

# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# server.info=Apache Tomcat/9.0.16 (Ubuntu)
# server.number=9.0.16.0
# server.built=Sep 11 2019 19:47:51 UTC

修改完成后,把修改完成的数据存储到catalina.jar中。

$ cd ~

$ cd catalina

$ sudo apt install openjdk-11-jdk-headless

$ jar uvf catalina.jar org/apache/catalina/util/ServerInfo.properties

把修改后的catalina.jar放回到Tomcat的目录下面:

$ cd ~

$ cd catalina

$ sudo unlink /usr/share/tomcat9/lib/catalina.jar 

$ sudo mv /usr/share/java/tomcat9-catalina.jar /usr/share/java/tomcat9-catalina.jar.old

$ sudo cp catalina.jar /usr/share/java/

$ sudo chmod +r /usr/share/java/catalina.jar

$ cd /usr/share/tomcat9/lib

$ sudo ln -s ../../java/catalina.jar catalina.jar

重启Tomcat的服务

$ sudo service tomcat9 restart

参考链接


在Ubuntu 16.04/18.04/20.04 LTS上安装OpenGrok-1.3.11/1.3.16/1.5.12/1.7.25浏览Android源码

更加推荐通过 Podman/Docker 方式进行部署,参考链接 Ubuntu 22.04使用Podman部署OpenGrok的详细教程

OpenGrok 是一个快速,便于使用的源码搜索引擎与对照引擎,它能够帮助我们快速的搜索、定位、对照代码树。接下来就具体讲解 `Ubuntu 16.04/18.04/20.04 LTS` 环境下 `OpenGrok` 的安装及使用。

`OpenGrok 1.3.11/1.3.16` 依赖 `Java 1.8` , `Tomcat 8`

`OpenGrok 1.5.12` 依赖 `Java 11` , `Tomcat 9` 。

从 `OpenGrok 1.6.0`开始依赖 `Java 11` , `Tomcat 10` 。

1.依旧参照 UBUNTU 13.10 APACHE 2.2 通过 AJP 整合 TOMCAT 7 中讲述的方法,进行 `Tomcat 8/9`,  `Apache 2.4`的配置安装,只不过路径中的 `Tomcat7` 目录替换成 `Tomcat8` (ubuntu 20.04 默认 `Tomcat9`)。

2.安装 `Tomcat 8` (ubuntu 18.04)

$ sudo apt-get install tomcat8

# 一般卸载 tomcat7
$ sudo apt-get remove --purge tomcat7*

# 调整内存,默认的128MB 默认配置运行会导致OOM
$ sudo touch /usr/share/tomcat8/bin/setenv.sh

$ echo 'export JAVA_OPTS="-Xms512M -Xmx1024M"' | sudo tee -a /usr/share/tomcat8/bin/setenv.sh

安装 `Tomcat 9` (ubuntu 20.04)

# 一般卸载 tomcat7/8
$ sudo apt-get remove --purge tomcat7*

$ sudo apt-get remove --purge tomcat8*

$ sudo apt-get install tomcat9

# 调整内存,默认的128MB 默认配置运行会导致OOM
# sudo touch /usr/share/tomcat9/bin/setenv.sh

$ echo 'export JAVA_OPTS="-Xms512M -Xmx1024M"' | sudo tee -a /usr/share/tomcat9/bin/setenv.sh

3.安装 `universal-ctags` 用于对 `C\C++` 代码的支持

给代码建立索引时,要使用到universal-ctags工具,但是一般通过apt-get安装的都是exuberant-ctags,所以要先删除原有的ctags版本,然后安装universal-ctags.

$ sudo apt-get purge ctags

$ sudo apt-get install git

$ sudo apt-get install autoconf

$ sudo apt-get install pkg-config

$ cd ~

$ git clone --depth=1 https://github.com/universal-ctags/ctags.git

$ cd ctags

$ ./autogen.sh 

$ ./configure

$ make

$ sudo make install

$ cd ..

$ rm -rf ctags

4.下载并安装OpenGrok

可以到"https://oracle.github.io/opengrok/"手工下载文件,然后上传到服务器,也可以直接用wget命令来下载,一般选择安装在"/opt"目录下面。

$ sudo apt-get install aria2

$ cd /opt

$ sudo aria2c -c "https://github.com/oracle/opengrok/releases/download/1.3.11/opengrok-1.3.11.tar.gz"

# sudo aria2c -c "https://github.com/oracle/opengrok/releases/download/1.3.16/opengrok-1.3.16.tar.gz"

# sudo aria2c -c "https://github.com/oracle/opengrok/releases/download/1.5.12/opengrok-1.5.12.tar.gz"

# sudo aria2c -c "https://github.com/oracle/opengrok/releases/download/1.7.25/opengrok-1.7.25.tar.gz"

解压缩文件到当前目录"/opt"

$ sudo tar xvf opengrok-1.3.11.tar.gz

# sudo tar xvf opengrok-1.3.16.tar.gz

# sudo tar xvf opengrok-1.5.12.tar.gz

# sudo tar xvf opengrok-1.7.25.tar.gz

创建一个软链接,方便后续的修改

$ sudo ln -s opengrok-1.3.11 /opt/opengrok

# sudo ln -s opengrok-1.3.16 /opt/opengrok

# sudo ln -s opengrok-1.5.12 /opt/opengrok

# sudo ln -s opengrok-1.7.25 /opt/opengrok

# 安装工具,必须是Python3 ,Python2 安装会失败
$ sudo apt-get install python3-pip

$ sudo pip3 install --upgrade pip

# 如果报错 ModuleNotFoundError: No module named 'pip._internal'
# 执行如下命令
# sudo python3 -m pip install --upgrade pip

# 解决 AttributeError: module 'setuptools.dist' has no attribute 'check_specifier'
$ sudo pip3 install --upgrade setuptools

$ sudo pip3 install /opt/opengrok/tools/opengrok-tools.tar.gz

链接"`/opt/opengrok/lib/source.war`"到 `Tomcat8` 的工程目录"`/var/lib/tomcat8/webapps/`",比如我们有多个源代码工程,建议进行链接操作。

如下:

$ sudo ln -s /opt/opengrok/lib/source.war /var/lib/tomcat8/webapps/Android_4.2.2.war

$ sudo ln -s /opt/opengrok/lib/source.war /var/lib/tomcat8/webapps/Android_7.0.0_r21.war

$ sudo ln -s /opt/opengrok/lib/source.war /var/lib/tomcat8/webapps/Android_10.0.0_r40.war

$ sudo ln -s /opt/opengrok/lib/source.war /var/lib/tomcat8/webapps/Chromium.war

访问"http://localhost:8080/source/"确认OpenGrok是否已经安装成功,如果安装成功,出现下面的界面:OpenGrok

5.出于安全原因,禁止外网访问Tomcat的8080端口

只允许Tomcat在本地的8080端口监听即可,修改

$ sudo vim /var/lib/tomcat8/conf/server.xml

添加 address="127.0.0.1"

<Connector port="8080" address="127.0.0.1" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />

重启Tomcat8

$ sudo service tomcat8 restart

6.配置Apache2对Tomcat通过AJP进行反向代理

`Apache2` 的配置文件“ `/etc/apache2/sites-enabled/000-default.conf`” (如果开启了HTTPS,则需要同步修改 `/etc/apache2/sites-enabled/000-default-le-ssl.conf` 或者 `/etc/apache2/sites-enabled/default-ssl.conf`) 中,增加如下配置:

<VirtualHost *:80>
	#for Tomcat 8 openGrok Android Source
	ProxyPass /Android_4.2.2/ ajp://127.0.0.1:8009/Android_4.2.2/
	ProxyPassReverse /AndroidXRef_4.2.2/ ajp://127.0.0.1:8009/AndroidXRef_4.2.2/
        
	ProxyPass /Android_7.0.0_r21/ ajp://127.0.0.1:8009/Android_7.0.0_r21/
	ProxyPassReverse /Android_7.0.0_r21/ ajp://127.0.0.1:8009/Android_7.0.0_r21/
	
	ProxyPass /Android_10.0.0_r40/ ajp://127.0.0.1:8009/Android_10.0.0_r40/
	ProxyPassReverse /Android_10.0.0_r40/ ajp://127.0.0.1:8009/Android_10.0.0_r40/		
	
	#for Tomcat 8 OpenGrok Chromium	
	ProxyPass /Chromium/ ajp://127.0.0.1:8009/Chromium/
	ProxyPassReverse /Chromium/ ajp://127.0.0.1:8009/Chromium/	
</VirtualHost>

`Tomcat8` 的配置文件/var/lib/tomcat8/conf/server.xml中增加如下配置<Context path="/Android_4.2.2" docBase="Android_4.2.2/"/>,解决跳转404问题。ProxyPass后面必须携带“/”,否则就会出现404问题。

<Host name="localhost"  appBase="webapps"
	unpackWARs="true" autoDeploy="true">
 
	<Context path="/Android_4.2.2" docBase="Android_4.2.2/"/>
	<Context path="/Android_7.0.0.r21" docBase="Android_7.0.0_r21/"/>
	<Context path="/Android_10.0.0_r40" docBase="Android_10.0.0_r40/"/>
	<Context path="/Chromium" docBase="Chromium/"/>

	<!-- SingleSignOn valve, share authentication between web applications
		 Documentation at: /docs/config/valve.html -->
	<!--
	<Valve className="org.apache.catalina.authenticator.SingleSignOn" />
	-->
 
	<!-- Access log processes all example.
		 Documentation at: /docs/config/valve.html
		 Note: The pattern used is equivalent to using pattern="common" -->
	<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
		   prefix="localhost_access_log." suffix=".txt"
		   pattern="%h %l %u %t &quot;%r&quot; %s %b" />

	<!-- 隐藏版本号 -->
	<Valve className="org.apache.catalina.valves.ErrorReportValve" showReport="false" showServerInfo="false" />

</Host>

7.配置OpenGrok对源代码进行解析

设置要建立索引的源代码目录的位置,以存储在"/data/OpenGrok/Android_4.2.2"上的Android代码为例子:

$ export OPENGROK_INSTANCE_BASE=/data/OpenGrok/Android_4.2.2/

$ export OPENGROK_WEBAPP_CONTEXT=Android_4.2.2

注意:

由于我们使用了Apache2的反向代理才需要设置 `OPENGROK_WEBAPP_CONTEXT`,如果没有设置反向代理,请不要设置。

`OPENGROK_WEBAPP_CONTEXT` 内容就是在 `Apache2` 中设置的 `ProxyPassReverse` 指定的参数。

在使用反向代理的时候如果不设置OPENGROK_WEBAPP_CONTEXT会导致在点击具体的变量定义的时候,出现404。

不使用反向代理的时候请只设置OPENGROK_INSTANCE_BASE

这个变量的本质功能就是在建立文件索引的时候,在链接头部增加 OPENGROK_WEBAPP_CONTEXT 指定的路径,比如原来的路径是 https://www.mobibrw.com/a.html ,那么指定路径后,就变成了 https://www.mobibrw.com/ Android_4.2.2/a.html

创建源代码索引

$ cd /data/OpenGrok/Android_4.2.2/src

# 删除源代码下面可能存在的".git",".svn"隐藏目录
$ find . -name .svn -print0 | xargs -0 rm -r -f

$ find . -name .git -print0 | xargs -0 rm -r -f

# 删除无法进行索引的文件
$ find . -name *.apk -print0 | xargs -0 rm -r -f

$ find . -name *.zip -print0 | xargs -0 rm -r -f

$ find . -name *.jar -print0 | xargs -0 rm -r -f

# 编译工具是没有必要的目录
$ rm -rf prebuilts

$ export IDX_ROOT=/data/OpenGrok/Android_4.2.2/

# Android 源代码目录
$ export IDX_SRC=${IDX_ROOT}src/

$ export IDX_DATA=${IDX_ROOT}data/

$ export IDX_CONF=${IDX_ROOT}etc/configuration.xml

# 清理以前生成的索引数据 
# 索引数据目录 
$ cd ${IDX_ROOT} 

$ rm -rf data 

# 日志目录 
$ rm -rf log 

# 配置文件目录 
$ rm -rf etc 

$ rm -rf logging.properties

$ mkdir etc

# 限制索引内存占用,避免建立索引过程中引发OOM,
# 如果依旧出现OOM,则在参数中指定 “-T 1” 限制只能使用一个线程,不允许并发,减少内存开销
# 其实由于磁盘IO是整个索引的性能瓶颈,因此多线程并不能缩短太多的时间,反而占用了太多的内存
# 因此限制只能使用一个线程,在大多数情况下,是没问题的
# 同时使用 "-m 256" 参数增加 Lucene 4.x 使用的缓存的大小,避免出现问题
# Android 10的源代码至少需要2GB内存才能成功索引
$ export JAVA_OPTS="-Xmx2048m"

$ opengrok-indexer -J=-Xmx2g -a /opt/opengrok/lib/opengrok.jar -- -m 256 -s $IDX_SRC -d $IDX_DATA -T 1 -H -P -S -G -W $IDX_CONF

执行时间在40分钟左右,执行完成 。(如果通过SSH远程登录,可能会出现中途连接断开的情况,原因为某项操作比较耗时,导致长时间没有数据通信,网络超时断开。 参考 Linux SSH保持连接(解决Broken pipe))生成的索引文件在源代码的"data"目录下面,重建索引的时候需要执行如下操作,才能再次建立索引

# 切换回到项目根目录
$ cd ${IDX_ROOT}

# 索引数据目录
$ rm -rf data

# 日志目录
$ rm -rf log

# 配置文件目录
$ rm -rf etc

$ rm -rf logging.properties

注意,请务必删除源代码中的"prebuilts"目录,这个目录下面存储的是一系列的编译工具,在浏览代码的时候,完全用不上,但是占据的磁盘空间确是巨大的。

注意,如果服务器上面的内存比较有限,请使用如下命令进行内存限制,否则建立索引的时候,会触发内存不足的情况:

# 早期版本使用如下参数限制内存
$ export JAVA_OPTS="-Xmx1024m"

# 最新版本OpenGrok-1.3.11/1.3.16使用-J=-Xmx2g 进行内存限制,Android 10的源代码至少需要2GB内存才能成功索引

修改OpenGrok配置文件

$ sudo vim /var/lib/tomcat8/webapps/Android_4.2.2/WEB-INF/web.xml

修改其中的

<context-param>
	<param-name>CONFIGURATION</param-name>
	<param-value>/var/opengrok/etc/configuration.xml</param-value>
	<description>Full path to the configuration file where OpenGrok can read it's configuration</description>
</context-param>

为具体的工程目录"/data/OpenGrok/Android_4.2.2",修改后的配置如下:

<context-param>
	<param-name>CONFIGURATION</param-name>
	<param-value>/data/OpenGrok/Android_4.2.2/etc/configuration.xml</param-value>
	<description>Full path to the configuration file where OpenGrok can read it's configuration</description>
</context-param>

刷新浏览器,可以看到Android_4.2.2的源码可以搜索出来了。

参考链接


升级Struts2之后报告HTTP Status 500 - java.lang.ClassNotFoundException: org.apache.jsp.index_jsp以及org.apache.jasper.JasperException: Unable to compile class for JSP

升级Struts22.3.20.1版本升级到2.5.5版本后可能报告如下错误:

HTTP Status 500 - Unable to compile class for JSP:

type Exception report

message Unable to compile class for JSP:

description The server encountered an internal error that prevented it from fulfilling this request.

exception

org.apache.jasper.JasperException: Unable to compile class for JSP:

An error occurred at line: [38] in the generated java file: [/var/lib/tomcat7/work/Catalina/localhost/Tools/org/apache/jsp/index_jsp.java]
The method getJspApplicationContext(ServletContext) is undefined for the type JspFactory

Stacktrace:
	org.apache.jasper.compiler.DefaultErrorHandler.javacError(DefaultErrorHandler.java:103)
	org.apache.jasper.compiler.ErrorDispatcher.javacError(ErrorDispatcher.java:366)
	org.apache.jasper.compiler.JDTCompiler.generateClass(JDTCompiler.java:468)
	org.apache.jasper.compiler.Compiler.compile(Compiler.java:378)
	org.apache.jasper.compiler.Compiler.compile(Compiler.java:353)
	org.apache.jasper.compiler.Compiler.compile(Compiler.java:340)
	org.apache.jasper.JspCompilationContext.compile(JspCompilationContext.java:657)
	org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:357)
	org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:390)
	org.apache.jasper.servlet.JspServlet.service(JspServlet.java:334)
	javax.servlet.http.HttpServlet.service(HttpServlet.java:727)
	org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter.doFilter(StrutsPrepareAndExecuteFilter.java:110)
note The full stack trace of the root cause is available in the Apache Tomcat/7.0.52 (Ubuntu) logs.

也有可能发生如下错误信息:

HTTP Status 500 - java.lang.ClassNotFoundException: org.apache.jsp.index_jsp

具体信息如下图:

1422265899_66497

比较诡异的是,在Tomcat 8的环境下,是可以正常运行的,但是在Tomcat 7环境下却会报错。造成这个现象的原因就是在引入的Jar包中包含了jsp-api.jar这个Jar包,只要在最后生成的war包中排除这个文件即可。

在阿里云的Ubuntu 14.04系统上解决Tomcat 7由于OOM(Out Of Memory)而被系统杀掉的问题

最近服务器上面一直出现Tomcat莫名奇妙的被系统杀掉,后来从系统的日志中找到如下信息:

Oct 10 02:55:16 AY130422143404983ad9 kernel: [451276.905623] Out of memory: Kill process 809 (java) score 241 or sacrifice child
Oct 10 02:55:16 AY130422143404983ad9 kernel: [451276.905684] Killed process 809 (java) total-vm:1291052kB, anon-rss:493732kB, file-rss:0kB

原来是系统内存不足,导致进程被杀掉了,网上搜了一下,解决方法有两个

1.限制Tomcat使用的内存

方法如下:

$ sudo vim /usr/share/tomcat7/bin/setenv.sh

在文件尾部增加如下配置:

#防止Tomcat的OOM
export JAVA_OPTS="-server -Xms512M -Xmx1024M"

然后重启Tomcat

$ sudo service tomcat7 restart

2.为阿里云服务器增加swap分区/swap文件,来解决物理内存不足的问题

阿里云的服务器默认没有开启交换分区,导致内存极易耗尽导致服务被杀死,解决方法就是手工增加一个交换文件,来解决这个问题。

#如果要修改或者调整交换文件的大小,需要先停止交换文件,然后才能调整
# sudo swapoff -a

#创建一个2G的文件,要求每个扇区512个字节
$ sudo dd if=/dev/zero of=/swaps_file bs=512 count=4194308

$ sudo chmod 0644 swaps_file

$ sudo mkswap /swaps_file

$ sudo swapon /swaps_file

$ sudo cp /etc/sysctl.conf /etc/sysctl.conf.bak.old

$ sudo sed -i 's/^vm.swappiness[ \t]*=[ \t]*0$/vm.swappiness = 60/g' /etc/sysctl.conf

$ sudo cat /proc/sys/vm/swappiness

$ sudo sysctl -w vm.swappiness=60

$ sudo cat /proc/sys/vm/swappiness

$ sudo sed -i '$a\/swaps_file swap swap defaults,discard 0  0' /etc/fstab

$ sudo cp /etc/rc.local /etc/rc.local.bak.old

$ sudo sed -i 's/^swapoff[ \t]*-a$/swapon -a/g' /etc/rc.local

参考链接


Tomcat 7使用AJP协议导致AJP端口被意外暴露给外网

使用Ubuntu 13.10 Apache 2.2 通过 AJP 整合 Tomcat 7中的方法配置了通过AJP协议来通过Apache进行访问的代理。

但是最近发现Tomcat有时候会崩溃掉。刚刚开始以为是正常的OOM,后来分析日志,并没有找到相关的记录,反倒是发现如下内容:

Jan 26, 2016 5:06:47 PM org.apache.coyote.ajp.AjpMessage processHeader
SEVERE: Invalid message received with signature 18245
Jan 26, 2016 5:06:48 PM org.apache.coyote.ajp.AjpMessage processHeader
SEVERE: Invalid message received with signature 5635
Jan 26, 2016 5:06:48 PM org.apache.coyote.ajp.AjpMessage processHeader
SEVERE: Invalid message received with signature 18245
Jan 26, 2016 5:06:48 PM org.apache.coyote.ajp.AjpMessage processHeader
SEVERE: Invalid message received with signature 3338
Jan 26, 2016 5:06:48 PM org.apache.coyote.ajp.AjpMessage processHeader
SEVERE: Invalid message received with signature 20304
Jan 26, 2016 5:06:48 PM org.apache.coyote.ajp.AjpMessage processHeader
SEVERE: Invalid message received with signature 20304
Jan 26, 2016 5:06:48 PM org.apache.coyote.ajp.AjpMessage processHeader
SEVERE: Invalid message received with signature 32768
Jan 26, 2016 5:06:48 PM org.apache.coyote.ajp.AjpMessage processHeader
SEVERE: Invalid message received with signature 30

于是感觉有些奇怪,因为AJP协议应该不会发生非常频繁的通信协议错误问题。结果尝试从外网连接TomcatAjp端口8009,发现竟然可以通过telnet连接成功!!说明端口意外暴露给了外网。

那么根据The AJP Connector中的介绍说明(注意address部分),如果没有指定IP地址的话,默认是绑定任意地址,这样就导致外网也可以访问这个端口。因此出于安全考虑,我们需要增加这个address的设置,并且绑定到127.0.0.1。最终结果如下:

<!-- Define an AJP 1.3 Connector on port 8009 -->
<Connector port="8009" protocol="AJP/1.3" address="127.0.0.1" redirectPort="8443" />

而我在配置的时候,恰恰少设置了address="127.0.0.1".这个这种错误有些低级啊。

IntelliJ IDEA 2016.2使用Spring 4.3.1.RELEASE,sockjs-1.1.1,stomp-1.2搭建基于Tomcat-7.0.68的WebSocket应用

接着上文IntelliJ IDEA 2016.2使用Spring 4.3.1.RELEASE搭建基于Tomcat-7.0.68的WebSocket应用

上文的最后我们说到,WebSocket是需要定时心跳的,否则会在一段时间后自动断开连接,而更重要的是,不是所有的浏览器都支持WebSocket,早期的IE 10之前的版本就是不支持的,而这一部分的设备其实是不算少的,而sockjs的出现,恰恰好来解决了这个问题。对于不支持WebSocket的浏览器,sockjs使用了多种方式来兼容这种情况,包括使用长轮询等方式,Spring更是内建支持这种方式。

下面我们看如何在上篇文章的基础上,增加对于sockjs的支持。

首先是STOMP的文档官网地址 http://stomp.github.io/
代码的地址为https://github.com/jmesnil/stomp-websocket,项目下面的lib/stomp.js就是我们想要的文件。也可以本站下载stomp.js.zip

接下来sockjs的代码地址https://github.com/sockjs/sockjs-client,项目下面的dist/sockjs-1.1.1.js就是我们想要的文件。也可以本站下载sockjs-1.1.1.js.zip

接下来我们把下载到的文件放到我们工程目录下面的web->resources->javascript目录下面,如下图:

stomp-websockjs-resources

接下来,添加我们需要的com.fasterxml.jackson.core:jackson-annotations:2.8.1,com.fasterxml.jackson.core:jackson-core:2.8.1,com.fasterxml.jackson.core:jackson-databind:2.8.1这三个jar包,增加的方式参照上一篇中对于javax.servlet:javax.servlet-api:3.1.0的操作方法。与上一篇的操作不同的是,这次添加的三个jar包,都要放到编译完成后的War包中。最后的结果如下图:
ToolsJacksonMaven

ToolsJacksonMavenWar

下面,我们开始进行代码的操作,我们在上篇文章中的src->Tools->WebSocket中新增两个源代码文件SockJsController.java,WebJsSocketConfig.java.如下图:

NewJavaSourcesForSockjs

其中的代码如下:
SockJsController.java

package Tools.WebSocket;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

@Controller
public class SockJsController {

        @MessageMapping("/hello")
        @SendTo("/hello/subscribe") /*貌似这个名字可以随意的,主要用在stomp.subscribe时候的名字*/
        public String Hello(String message) throws Exception {
            return new String("Hello");
        }
}

WebJsSocketConfig.java

package Tools.WebSocket;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;


@Configuration
@EnableWebSocketMessageBroker
public class WebJsSocketConfig  extends AbstractWebSocketMessageBrokerConfigurer {
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/webSocketServer");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/hello").setAllowedOrigins("*").withSockJS();
    }
}

然后修改WebSocket.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>WebSocket/SockJS Echo Sample (Adapted from Tomcat's echo sample)</title>
    <style type="text/css">
        #connect-container {
            float: left;
            width: 400px
        }

        #connect-container div {
            padding: 5px;
        }

        #console-container {
            float: left;
            margin-left: 15px;
            width: 400px;
        }

        #console {
            border: 1px solid #CCCCCC;
            border-right-color: #999999;
            border-bottom-color: #999999;
            height: 170px;
            overflow-y: scroll;
            padding: 5px;
            width: 100%;
        }

        #console p {
            padding: 0;
            margin: 0;
        }
    </style>

    <script src="./resources/javascript/sockjs-1.1.1.js"></script>
    <script src="./resources/javascript/stomp.js"></script>

    <script type="text/javascript">
        var ws = null;
        var url = null;
        var transports = [];
        var stompClient = null;

        function setConnected(connected) {
            document.getElementById('connect').disabled = connected;
            document.getElementById('disconnect').disabled = !connected;
            document.getElementById('echo').disabled = !connected;
        }

        function connect() {
            if (!url) {
                alert('Select whether to use W3C WebSocket or SockJS');
                return;
            }

            ws = (url.indexOf('sockjs') != -1) ?
                    new SockJS(url,undefined, {transports: transports}) : new WebSocket(url);
            if((url.indexOf('sockjs') != -1)) {
                stompClient = Stomp.over(ws);
                stompClient.connect({}, function(frame) {
                    setConnected(true);
                    log('Connected: ' + frame);
                    stompClient.subscribe('/hello/subscribe', function(message){
                        log(message.body);
                    });
                });
            }else {
                ws.onopen = function () {
                    setConnected(true);
                    log('Info: connection opened.');
                };

                ws.onmessage = function (event) {
                    log('Received: ' + event.data);
                };

                ws.onclose = function (event) {
                    setConnected(false);
                    log('Info: connection closed.');
                    log(event);
                };
            }
        }

        function disconnect() {
            if (ws != null) {
                ws.close();
                ws = null;
            }
            stompClient = null;
            setConnected(false);
        }

        function echo() {
            if(stompClient != null){
                var message = document.getElementById('message').value;
                stompClient.send("/webSocketServer/hello", {}, message);
                log('Sent: ' + message);
            }else {
                if (ws != null) {
                    var message = document.getElementById('message').value;
                    log('Sent: ' + message);
                    ws.send(message);
                } else {
                    alert('connection not established, please connect.');
                }
            }
        }

        function updateUrl(urlPath) {
            if (urlPath.indexOf('sockjs') != -1) {
                url = urlPath;
                document.getElementById('sockJsTransportSelect').style.visibility = 'visible';
            }
            else {
                if (window.location.protocol == 'http:') {
                    url = 'ws://' + window.location.host + urlPath;
                } else {
                    url = 'wss://' + window.location.host + urlPath;
                }
                document.getElementById('sockJsTransportSelect').style.visibility = 'hidden';
            }
        }

        function updateTransport(transport) {
            transports = (transport == 'all') ?  [] : [transport];
        }

        function log(message) {
            var console = document.getElementById('console');
            var p = document.createElement('p');
            p.style.wordWrap = 'break-word';
            p.appendChild(document.createTextNode(message));
            console.appendChild(p);
            while (console.childNodes.length > 25) {
                console.removeChild(console.firstChild);
            }
            console.scrollTop = console.scrollHeight;
        }
    </script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websockets
    rely on Javascript being enabled. Please enable
    Javascript and reload this page!</h2></noscript>
<div>
    <div id="connect-container">
        <input id="radio1" type="radio" name="group1" onclick="updateUrl('/webSocketServer');">
        <label for="radio1">W3C WebSocket</label>
        <br>
        <input id="radio2" type="radio" name="group1" onclick="updateUrl('/webSocketServer/sockjs/hello');">
        <label for="radio2">SockJS</label>
        <div id="sockJsTransportSelect" style="visibility:hidden;">
            <span>SockJS transport:</span>
            <select onchange="updateTransport(this.value)">
                <option value="all">all</option>
                <option value="websocket">websocket</option>
                <option value="xhr-polling">xhr-polling</option>
                <option value="jsonp-polling">jsonp-polling</option>
                <option value="xhr-streaming">xhr-streaming</option>
                <option value="iframe-eventsource">iframe-eventsource</option>
                <option value="iframe-htmlfile">iframe-htmlfile</option>
            </select>
        </div>
        <div>
            <button id="connect" onclick="connect();">Connect</button>
            <button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
        </div>
        <div>
            <textarea id="message" style="width: 350px">Here is a message!</textarea>
        </div>
        <div>
            <button id="echo" onclick="echo();" disabled="disabled">Echo message</button>
        </div>
    </div>
    <div id="console-container">
        <div id="console"></div>
    </div>
</div>

</body>
</html>

最后,我们修改web->WEB-INF->web.xml,在其中增加

<servlet-mapping>
	<servlet-name>dispatcher</servlet-name>
	<url-pattern>/webSocketServer/sockjs/*</url-pattern>
</servlet-mapping>

修改后的最终结果如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>
    <filter>
        <filter-name>struts2</filter-name>
        <filter-class>Tools.Filter.StrutsPrepareAndExecuteFilterEx</filter-class>
        <async-supported>true</async-supported>
    </filter>
    <filter-mapping>
        <filter-name>struts2</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
        <async-supported>true</async-supported>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>*.form</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/webSocketServer/*</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/webSocketServer/sockjs/*</url-pattern>
    </servlet-mapping>
</web-app>

参考链接