PageHelper自动增加limit分页问题

问题
使用PageHelperi分页,报错如下:
Caused by: com.alibaba.druid.sql.parser.ParserException: syntax error, error in :’limi1 1 limit ?,?’,expect LIMIT, actual LIMIT limit
原始查询sql(select * from t limit 1)后面有limit1,结果报错显示自动添加上上limit ?,?:

排查

见PageHelper 安全调用:https://github.com/pagehelper/Mybatis-PageHelper/blob/master/blob/master/wikis/zh/HowToUse.md#3-pagehelper-安全调用

PageHelper 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的。

只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal 存储的对象。

线程中start的page 不能保证线程在当前执行退出时清理完page变量

重现

XXXServiceImpl {
     XX method(Object xx, int pageNum, int pageSize) {
          PageHelper.start(pageNum,pageSize);
          if(xx!=null){
             return XX;
          }
          xxxMapper.find();
            ....
       }
 }
在执行PageHelper.start(pageNum,pageSize);方法后,参数page变量,如xx!=null,直接返回XX,则page没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,如果接下来执行其它sql,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。

如果PageHelper.start(pageNum,pageSize);之后的方法加了缓存,也会有这个问题。

解决方法 

1.使用参数方式是极其安全的2.保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,必须保证分页和查询同时有效。3.调PageHelper.clearPage(); 可以手动清理 ThreadLocal 存储的分页参,这个是新版本里的方法 其中5.0之后版本注意:

1. 配置文件中PageHelper变为了PageInterceptor
<plugin interceptor=”com.github.pagehelper.PageInterceptor”>
2. 不需要<property name=”dialect” value=”mysql”/>,自动识别数据库

Spring基于annotation的缓存使用总结

缓存使用思路

我们使用缓存,一般的思路是这样的:一个业务查询方法,我们先去查询缓存,如果命中缓存,则直接返回结果。如果缓存没有命中,再去查询数据库,然后将结果存入缓存,下一次再执行这个方法时,如果缓存没有过期,则直接返回缓存数据。(注:这里没考虑并发情况)。示例代码如下:

  public User getUserByName(String name) {
    // 首先查询缓存
    User result = redisHelper.get(name);
    if(result !=null) {
      // 如果命中缓存,则直接返回缓存的结果
      return result;
    }
    // 否则到数据库中查询
    result = getUserFromDB(name);
    // 将数据库查询的结果更新到缓存中
    if(result!=null) {}
      redisHelper.put(name, result);
    }
    return result;
  }

这种缓存方案的劣势

  • 缓存代码和业务代码耦合度太高,不便于维护和变更,代码可读性也差

  • 这种缓存方案不支持按照某种条件的缓存,比如有某种类型的User才需要缓存

基于annotation的缓存

我们使用Spring的基于annotation的缓存技术,通过在既有代码中添加少量它定义的各种annotation,就可以达到上述效果,而且还可以使用SpEL(Spring Expression Language来定义缓存的key和各种condition,按照某种条件进行缓存。

1、@Cacheable

  // 使用了一个缓存名叫userCache
  @Cacheable(value="userCache")
  public User getUserByName(String name) {
    // 方法内部实现不考虑缓存逻辑,直接实现业务
    User user =  getUserFromDB(name);
    return user;
  }

这里用到了Spring的一个annotation,即@Cacheable(value=”userCache”),它的意思是,当调用这个方法的时候,会从一个名叫userCache 的缓存中查询,如果没有,则执行实际的方法(即查询数据库),并将执行的结果存入缓存中,否则直接返回缓存中的结果。注意:这里的value=”userCache”不是指缓存中的 key,这里缓存的key 是方法的参数name"userCache"〔拼音〕是指定义这个缓存的名称。

condition :如何按照条件操作缓存

前面的缓存方法,没有任何条件,即所有对 getUserByName 方法的调用都会起动缓存效果,如果有一个需求,就是只有账号名称的长度小于等于 4 的情况下,才做缓存,如何实现?Spring提供了一个很好的方法,那就是基于 SpEL 表达式的 condition 定义,这个 condition 是 @Cacheable注解的一个属性。

    @Cacheable(value="userCache",condition="#name.length() <= 4")
    public User getUserByName(String name)...

注意其中的 condition=”#name.length() <=4”,条件表达式返回一个布尔值,当条件为 true,则进行缓存操作,否则直接调用方法执行的返回结果。

如果有多个参数,如何进行 key 的组合

这里我们需要根据name、password对User对象进行缓存,我们可以利用 SpEL 表达式对缓存 key 进行设计。

    @Cacheable(value="userCache",key="#name.concat(#password)")
    public User getUser(String name,String password)...

也可以直接用+拼接:

 @Cacheable(value = "doctor:app", key = "'listPatientTags:'+#userCode+':'+#weimaihao")
 public List<PatientTagVo> listPatientTags(Long userCode, Long weimaihao)...

2、@CacheEvict:清空缓存

使用@Cacheable,可以完成基本的缓存查询,但当User数据发生变更,那么必须要清空缓存,以保证缓存数据的可靠性。有两种情况:

    1. 清空此user对应的缓存

    1. 清空所有缓存

  public class UserService {
    @Cacheable(value="userCache")
    public User getUserByName(String name) {
      return getUserFromDB(name);
    }
    // 清空key为user.getName()的缓存
    @CacheEvict(value="userCache",key="#user.getName()")
    public void updateUser(User user) {
      ...
    }
    // 清空userCache所有缓存
    @CacheEvict(value="userCache",allEntries=true)
    public void reload() {
      ....
    }
  }

由此可见,清空缓存的方法,就是通过 @CacheEvict 来标记要清空缓存的方法,当这个方法被调用后,即会清空缓存。

注意其中@CacheEvict(value=”userCache”,key=”#user.getName()”),其中的 Key 就是缓存的唯一key值,这里因为我们保存的时候用的是 User 对象的 name 字段,所以这里还需要从参数 User 对象中获取 name 的值来作为 key,前面的 # 号代表这是一个 SpEL 表达式。

@CacheEvict 的可靠性问题

@CacheEvict有一个属性 beforeInvocation,缺省为 false,即缺省情况下,都是在实际的方法执行完成后,才对缓存进行清空操作。期间如果执行方法出现异常,则会导致缓存清空不被执行。

@CacheEvict(value="userCache",allEntries=true)
public void reload() {
  throw new RuntimeException();
}

注意上面的代码,我们在 reload 的时候抛出了运行期异常,这会导致清空缓存失败。如何避免这个问题呢?我们可以用 @CacheEvict 注释提供的 beforeInvocation 属性,将其设置为 true,这样,在方法执行前我们的缓存就被清空了。

@CacheEvict(value="userCache",allEntries=true,beforeInvocation=true)
public void reload() {
  throw new RuntimeException();
}

3、@CachePut :既要保证方法被调用,又希望结果被缓存

当用@Cacheable注解时,如果重复使用相同参数调用方法的时候,方法本身不会被执行,而是直接从缓存中返回,但实际需求中,有些情况下我们希望方法一定会被调用,因为其除了返回一个结果,还做了其他事情,例如记录日志,调用接口等,这个时候,我们可以用 @CachePut,这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中。

    @Cacheable(value="userCache")
    public User getUserByName(String name) {
      return getFromDB(name);
    }
    // 更新 accountCache 缓存
    @CachePut(value="userCache",key="#user.getName()")
    public User updateUser(User user) {
      ...
    }

如上面的代码所示,我们首先用 getUserByName 方法查询,这个时候会查询数据库一次,同时结果也记录到缓存中了。然后我们调用了 updateUser 方法,这个时候会执行数据库的更新操作且记录到缓存。@CachePut 可以保证方法被执行,且结果一定会被缓存。

@Cacheable、@CachePut、@CacheEvict 总结

通过上面的例子,我们可以看到 spring cache 主要使用以下几个注解标签,即 @Cacheable、@CachePut 和 @CacheEvict,总结一下其作用和配置方法。

    1. @Cacheable :能够根据方法的请求参数对其结果进行缓存,主要参数:

    • value:缓存的名称,可以多个:如@Cacheable(value={”cache1”,”cache2”}

    • key: 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则默认按照方法的所有参数进行组合

    • condition:缓存的条件,可以为空,返回 true 或者 false,只有为 true 才进行缓存

    1. @CachePut :能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用。主要的参数:

    • value:同Cacheable

    • key:同Cacheable

    • condition:同Cacheable

    1. @CacheEvict :主要针对方法配置,能够根据一定的条件对缓存进行清空。主要的参数:

    • value:同Cacheable

    • key:同Cacheable

    • condition:同Cacheable

    • allEntries:是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存。例如: @CachEvict(value=”testcache”,allEntries=true)

    • beforeInvocation 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 例如: @CachEvict(value=”testcache”,beforeInvocation=true)

笔记:SpringBoot如何集成Maven的profile功能

什么是profile

开发项目的时候要有多个环境,如开发环境、测试环境、生产环境,他们的配置文件一般不同。当我们要向各个环境发布程序时,需要人工处理这些配置文件,这显然麻烦且易错。有了profile,一切问题就简单了。只要在maven打包时使用下面命令即可。

 mvn clean package -Dmaven.test.skip=true -P prod

-P prod 就是告诉maven要使用名字为prod的profile来打包,这样打包后的jar包或war包,配置文件里的配置就是prod环境的。

实现思路

maven支持profile功能,当使用maven profile打包时,可以打包指定目录和指定文件,且可以修改文件中的变量。spring boot也支持profile功能,只要在application.properties文件中指定spring.profiles.active=xxx 即可,其中xxx是一个变量,当maven打包时,修改这个变量即可。

具体实现

一个springboot项目,工程中src/main/resourses目录下有以下4个配置文件:

  • application.properties: 包含通用配置的文件。文件中有spring.profiles.active=@profiles.active@的属性。这里的profiles.active和下面pom.xml配置中的profiles.active属性名要一致。这样,在运行mvn命令时,maven就会帮我们将@profiles.active@替换成指定的profile。
  • application-dev.properties: 当-P dev时, 打包这个文件。
  • application-test.properties: 当 -P test时,打包这个文件。
  • application-prod.properties: 当 -P prod时,打包这个文件。

在(父)pom.xml中定义maven的如下各个环境的profile配置,这里的profiles.active属性名可以自定义,其中test环境是默认激活的(activeByDefault为true),这样如果在不指定profile时默认是test环境。

  <profiles>
        <profile>
            <id>dev</id>
            <properties>
                <profiles.active>dev</profiles.active>
            </properties>
        </profile>
        <profile>
            <id>test</id>
            <properties>
                <profiles.active>test</profiles.active>
            </properties>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
        </profile>
        <profile>
            <id>pre</id>
            <properties>
                <profiles.active>pre</profiles.active>
            </properties>
        </profile>
        <profile>
            <id>prod</id>
            <properties>
                <profiles.active>prod</profiles.active>
            </properties>
        </profile>
    </profiles>

    <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <excludes>
					<exclude>application-dev.properties</exclude>
					<exclude>application-test.properties</exclude>
					<exclude>application-pre.properties</exclude>
					<exclude>application-prod.properties</exclude>
				</excludes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <includes>
					<include>application-${profiles.active}.properties</include>
					<include>application.properties</include>
				</includes>
            </resource>
        </resources>
    </build>

说明:

  1. 这里的excludes 表示打包时,过滤多余其它目录或文件,比如过滤目录:
                <excludes>
					<exclude>dev/*</exclude>
					<exclude>test/*</exclude>
					<exclude>pre/*</exclude>
					<exclude>prod/*</exclude>
				</excludes>
  1. includes表示打包,指定要包含的文件,其中${profiles.active}会根据指定的profile动态替换,当然一般不需要过滤或指定,如下配置就可以了:
 <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>
  1. filtering 设置true表示处理文件时,可以对文件进行动态替换
  2. 在application.properties属性文件中替换变量时,使用@符号,而不是$, maven的maven-resources-plugin可以定义这个替换的符号。
spring.profiles.active=@profiles.active@
  1. 怎么在启动spring boot应用时,打印正在使用的profile,避免配置错误
public static void main(String[] args) {
  ApplicationContext ctx = SpringApplication.run(RestApiApplication.class, args);
  String[] activeProfiles = ctx.getEnvironment().getActiveProfiles();
  for (String profile : activeProfiles) {
    logger.warn("Spring Boot 使用profile为:{}" , profile);
  }
}

参考:

配置管理平台Apollo搭建指南

Acheron注:

Apollo 的官方文档写得非常友好全面,官方地址:https://github.com/ctripcorp/apollo/wiki

这里只是记录一下我在服务器上搭建的过程。

一、环境要求

  • Java: 1.8+
    • 检查:java -version
  • MySql:5.6.5+
    • 检查:SHOW VARIABLES WHERE Variable_name = 'version';

二、部署步骤

2.1 导入数据库生成脚本

官方提供的数据库脚本有两个,在scripts/sql下,apolloportaldb.sqlapolloconfigdb.sql

// 创建:ApolloPortalDB
source /your_local_path/sql/apolloportaldb.sql
// 创建:ApolloConfigDB
source /your_local_path/sql/apolloconfigdb.sql
// 检查是否导入成功:
select `Id`, `Key`, `Value`, `Comment` from `ApolloPortalDB`.`ServerConfig` limit 1;
select `Id`, `Key`, `Value`, `Comment` from `ApolloConfigDB`.`ServerConfig` limit 1;

2.2 Apollo自身的一些配置

2.2.1 配置ApolloPortalDB.ServerConfig

  • 1.apollo.portal.envs – 可支持的环境列表:

默认值是dev,多个以逗号分隔即可(大小写不敏感),如:

DEV,FAT,UAT,PRO

注意:只在数据库添加环境是不起作用的,需要配合修改scripts/build.sh,添加新增环境对应的meta server地址。

  • organizations – 部门列表:
[{"orgId":"TEST1","orgName":"样例部门1"},{"orgId":"TEST2","orgName":"样例部门2"}]
  • wiki.address: portal上“帮助”链接的地址,默认是Apollo github的wiki首页。

####2.2.2 配置ApolloConfigDB.ServerConfig

  • 1.eureka.service.url – Eureka服务Url,如有多个,用逗号分隔(注意不要忘了/eureka/后缀):
http://1.1.1.1:8080/eureka/,http://2.2.2.2:8080/eureka/

2.3 配置数据库连接信息

  • vim scripts/build.sh
#apollo config db info
apollo_config_db_url=jdbc:mysql://localhost:3306/ApolloConfigDB?characterEncoding=utf8
apollo_config_db_username=用户名
apollo_config_db_password=密码(如果没有密码,留空即可)

# apollo portal db info
apollo_portal_db_url=jdbc:mysql://localhost:3306/ApolloPortalDB?characterEncoding=utf8
apollo_portal_db_username=用户名
apollo_portal_db_password=密码(如果没有密码,留空即可)

2.4 配置各环境meta service地址

  • vim scripts/build.sh:修改各环境meta service服务地址。 如果某个环境不需要,也可以直接删除对应的配置项
dev_meta=http://localhost:8080
fat_meta=http://localhost:8080
uat_meta=http://localhost:8080
pro_meta=http://localhost:8080

2.5 执行编译、打包

./build.sh 

2.6 部署运行

2.6.1 部署apollo-configservice

apollo-configservice/target/目录下的apollo-configservice-x.x.x-github.zip上传到服务器上,解压后执行scripts/startup.sh即可。如需停止服务,执行scripts/shutdown.sh.

cd apollo-configservice/target/
scp apollo-configservice-x.x.x-github.zip 209:/opt/apollo/config
ssh 209
cd /opt/apollo/config
unzip apollo-configservice-x.x.x-github.zip
运行:./scripts/startup.sh
停止:./scripts/shutdown.sh

注:如要调整服务的监听端口,可以修改startup.sh中的SERVER_PORT。另外apollo-configservice同时承担meta server职责,如果要修改端口,注意要同时修改scripts/build.sh中的meta server url信息以及ApolloConfigDB.ServerConfig表中的eureka.service.url配置项。

2.6.2 部署apollo-adminservice

同上:

apollo-adminservice/target/目录下的apollo-adminservice-x.x.x-github.zip上传到服务器上,解压后执行scripts/startup.sh即可。如需停止服务,执行scripts/shutdown.sh.

注:如要调整服务的监听端口,可以修改startup.sh中的SERVER_PORT

2.6.3 部署apollo-portal

同上:

apollo-portal/target/目录下的apollo-portal-x.x.x-github.zip上传到服务器上,解压后执行scripts/startup.sh即可。如需停止服务,执行scripts/shutdown.sh.

apollo-portal的默认端口是8080,和apollo-configservice一致,所以如果需要在一台机器上同时启动apollo-portal和apollo-configservice的话,需要修改apollo-portal的端口。直接修改startup.sh中的SERVER_PORT即可,如SERVER_PORT=8070

三、Java客户端使用

3.1 配置Appid

classpath:/META-INF/app.properties文件:

app.id=YOUR-APP-ID

3.2 配置Environment

  • 对于Mac/Linux,文件位置为/opt/settings/server.properties
  • 对于Windows,文件位置为C:\opt\settings\server.properties

保证settings目录文件权限:chmod 777 /opt/settings

文件内容形如:

env=DEV

3.3 配置本地缓存路径

本地缓存路径位于以下路径,所以请确保/opt/dataC:\opt\data\目录存在,且应用有读写权限。

  • Mac/Linux: /opt/data/{appId}/config-cache
  • Windows: C:\opt\data{appId}\config-cache

保证data目录文件权限:chmod 777 /opt/data

3.4 配置日志地址

保证logs目录文件权限:chmod 777 /opt/logs

3.5 Maven Dependency

<dependency>
        <groupId>com.ctrip.framework.apollo</groupId>
        <artifactId>apollo-client</artifactId>
        <version>0.7.0</version>
 </dependency>

3.6 使用

@Configuration
@EnableApolloConfig
public class AppConfig {
  
}
@ApolloConfig
private Config config;

@Test
public void testApollo(){
	String name = config.getProperty("name", "hello");
    assertEquals(name,"Acheron");
}

手把手教你实现一个网页聊天室

现在写文章都喜欢“手把手”,有一种老爸抱着三岁的儿子在路边撒尿的感觉,所以这篇的题目我也“手把手”一回,费话不说,开始撒尿:

使用到的技术

  • websocket
  • springmvc
  • maven
  • fastjson

这里使用到的主要技术是websocket,后端服务用java来写,其中springmvcmaven只是用于写java时的框架和构建工具,fastjson用于json数据格式的转换。

什么是websocket?

websocket是html5提供的一种在一个(TCP)接口进行全双工通信的技术,所谓全双工通信,简单点说就是通信的双方都可以同时向对方发送数据,类似于打电话时两方可以同时说话。

在没有websocket之前,实现聊天室这种实时的消息响应需求,一般会用长连接轮询的方式,在特定的的时间间隔(如每1秒),由客户端对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端,这种方式有很明显的缺点,客户端需要不断的向服务器发出请求,然而HTTP请求的header是非常长的,而我们需要的有用数据可能只是一个很小的值,这样会占用很多的带宽。而用websocket,浏览器和服务器只需要做一个握手的动作,两者之间就形成了一条快速通道,直接就可以互相传送数据了。

支持websocket的浏览器和服务器

websocket是html5的特性,浏览器至少要支持html5,服务器我这边用tomcat7.0.54(其它较高版本我这边不起作用!?),另外java版本要7以上:

浏览器支持 版本
Chrome 4+
Firefox 4+
Internet Explorer 10+
Opera 10+
Safari 5+
服务器支持 版本
tomcat 7.0.27+
jetty 7.0.1+
Nginx 1.3.13+
resin 4+

开始代码

websocket浏览器实现

浏览器通过javascript向服务器发起websocket请求,获取websocket连接后,浏览器和服务器就可以通过TCP连接直接交换数据了。

首先需要创建一个websocket对象:

var socket = new WebSocket('ws://localhost:8080/chatsocket');

这里的参数指定后台websocket服务的连接地址,不同于http://,websocket请求地址以ws://开头,接下来是主机的地址+端口号+后台服务名,这里的chatsocket是后面java后端所起的服务名字。其实第一次的握手连接,套接字还是以HTTP连接开始的,在HTTP握手之后才升级为TCP套接字,任意一端就都可以发送数据了。如果页面用得是JSP模板,地址和端口可以用JSP的pageContext获取,而不要直接写死:

  • 获取地址:${pageContext.request.serverName}
  • 获取端口:${pageContext.request.serverPort}
  • 获取上下文路径:${pageContext.request.contextPath}

websocket对象的方法

创建websocket对象后,它有一些相关属性、事件和方法,具体可以查看W3C的websocket API

比较重要的事件和方法主要有以下几个:

//连接成功建立的回调方法
socket.onopen = function (event) { console.log("连接服务器成功!"); };
//点击发送消息的方法,这里以json格式传送数据
socket.send(JSON.stringify({
    content: "这里是消息的内容"
    name:"发送人"
}));
//接收到消息的回调方法,一般将返回的数据append到消息页面
socket.onmessage = function (event) { 
    var message = JSON.parse(event.data);
    //dosomething...
    //发送人:message.name ,发送时间: message.date,发送内容:message.content
};

websocket服务器实现

第一步:加入信赖

java要使用websocket要加入相关信赖:

<dependency>
    <groupId>javax.websocket</groupId>
    <artifactId>javax.websocket-api</artifactId>
    <version>1.1</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-websocket</artifactId>
    <version>4.0.1.RELEASE</version>
</dependency>

注意:

  • tomcat的lib目录下有一个包websocket-api.jar,用这个包就可以了,所以部署的时候将上述的javax.websocket-api.jar删除再重新启动,否则两则重复,启动会报错。
  • spring-websocket-4.0.1.RELEASE.jar这个包的作用只是需要在websocket后端注入service所用的,否则删除这个包不影响websocket服务运行。
  • 具体spring对websocket的支持见这里

第二步:开启后端服务

新建一个java类,在这个类上加上ServerEndpoint注解,它就变成websocket服务了:

@ServerEndpoint(value = "/chatsocket",configurator = SpringConfigurator.class)
public class HudongEndpoint{

}

这里定义了两个参数:

  • value:服务的名字,前面js创建websocket时的url的服务名就是这个
  • configurator:如果要用spring注入servie,就要加上这个参数

第三步:接收消息的方法

和html5的websocket一样,后端也有相关操作方法,加上相关的注解就可以,比如:

  • @OnOpen:连接建立成功时调用的方法
  • @OnMessage:收到客户端消息后调用的方法
  • @OnClose:连接关闭调用的方法
  • @OnError:发生错误时调用的方法

这里主要说明下@OnMessage方法:

@OnMessage
public void getMessage(String message, Session session) {
    JSONObject jsonObject= JSONObject.parseObject(message);

    jsonObject.put("date", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
    for (Session openSession : session.getOpenSessions()) {
        jsonObject.put("isSelf", openSession.equals(session));
        openSession.getAsyncRemote().sendText(jsonObject.toString());
        //openSession.getBasicRemote().sendText(message);
    }
}

这个方法有两个参数:

  • message:前端发送过来的消息,json格式
  • session:这个参数可选,为与某个客户端的连接会话,需要通过它给客户端发送数据。如果要将聊天数据保存到数据库中,可以在这个方法中执行相关操作

总结

整个websocket发送消息和接收消息的过程,和http请求在使用上其实没多大区别,后台的一个ServerEndpoint类似于 springmvc的一个controller,相关的业务操作在对应的方法里就是了。这里只是简单的文本聊天,如果要加上表情和图片,可以使用富文本编辑器(Ueditor,kindeditor等),当然还可以加上计算实时在线人数,上线下线通知等功能。