SpringBoot与服务整合
大约 25 分钟
定时任务
Spring 定时任务
Spring 定时任务
- 在项目中经常需要在某一时刻由系统自动的实现一些任务的处理(例如:定时数据抓取凌晨 00:00 进行数据处理)这样的机制称为任务调度,在 Spring 开发框架中提供了 SpringTask 工具组件用于实现定时任务的开发需求

1、
package com.yootk.task;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
@Component // 任务的处理类需要提供有一个组件
@Slf4j // 直接通过日志输出任务信息
public class YootkScheduleTask { // 定义任务处理类
@Scheduled(fixedDelay = 2000) // 每2秒触发一次当前的任务
public void runJobA() { // 间隔任务
log.info("【RATE任务】{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()));
}
@Scheduled(cron = "* * * * * ?") // 每秒触发一次CRON任务
public void runJobB() { // CRON任务
log.info("【CRON任务】{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()));
}
}
2、
package com.yootk; // 父包,这个包中的所有子包的类会被自动扫描
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication // 一个注解解决所有的问题
@EnableScheduling // 启动定时任务调度
public class StartSpringBootApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(StartSpringBootApplication.class,args); // 运行SpringBoot程序
}
}
3、
package com.yootk.task;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Component // 任务的处理类需要提供有一个组件
@Slf4j // 直接通过日志输出任务信息
public class YootkScheduleTask { // 定义任务处理类
@Scheduled(fixedDelay = 2000) // 每2秒触发一次当前的任务
public void runJobA() throws InterruptedException { // 间隔任务
log.info("【RATE任务】{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()));
TimeUnit.SECONDS.sleep(5); // 强制模拟5秒的延迟
}
@Scheduled(cron = "* * * * * ?") // 每秒触发一次CRON任务
public void runJobB() { // CRON任务
log.info("【CRON任务】{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()));
}
}
4、
package com.yootk.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import java.util.concurrent.Executors;
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {// 定时任务配置
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(2)); // 允许两个任务并行执行
}
}
ShedLock 分布式定时任务
分布式任务调度
- ShedLock 是一个在分布式应用环境下的使用的定时任务管理框架,主要的目的是解决在分布式环境中多个实例相同定时任务在同一时间点的重复执行问题

分布式定时任务管理
- 由于所有的定时任务分布在集群中的不同节点之中,所以就需要有一个专属的数据存储空间用于清楚的记录下每一个定时任务的名称以及当前执行任务的主机与任务执行时间而后在集群中不同的节点执行任务前会首先查看数据存储中是否存在有指定的任务记录如果没有相应数据则可以启动该节点任务,反之,如果已经保存有此任务的相关信息则代表当前的任务正在执行,则要跳过该节点的定时任务

1、
/usr/local/redis/bin/redis-server /usr/local/redis/conf/redis.conf
2、
// https://mvnrepository.com/artifact/net.javacrumbs.shedlock/shedlock-spring
implementation group: 'net.javacrumbs.shedlock', name: 'shedlock-spring', version: '4.23.0'
// https://mvnrepository.com/artifact/net.javacrumbs.shedlock/shedlock-spring
implementation group: 'net.javacrumbs.shedlock', name: 'shedlock-spring', version: '5.12.0'
// https://mvnrepository.com/artifact/net.javacrumbs.shedlock/shedlock-provider-redis-spring
implementation group: 'net.javacrumbs.shedlock', name: 'shedlock-provider-redis-spring', version: '4.23.0'
// https://mvnrepository.com/artifact/net.javacrumbs.shedlock/shedlock-provider-redis-spring
implementation group: 'net.javacrumbs.shedlock', name: 'shedlock-provider-redis-spring', version: '5.12.0'
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis', version: '2.4.5'
// https://mvnrepository.com/artifact/org.apache.commons/commons-pool2
implementation group: 'org.apache.commons', name: 'commons-pool2', version: '2.9.0'
// https://mvnrepository.com/artifact/org.apache.commons/commons-pool2
implementation group: 'org.apache.commons', name: 'commons-pool2', version: '2.12.0'
3、
ext.versions = [ // 定义所有要使用的版本号
springboot : '2.4.3', // SpringBoot版本号
junit : '5.7.1', // 配置JUnit测试工具的版本编号
junitPlatformLauncher : '1.7.1', // JUnit测试工具运行平台版本编号
lombok : '1.18.18', // Lombok插件对应的版本号
fastjson : '1.2.75', // FastJSON组件对应的版本号
jackson : '2.12.2', // 配置Jackson相关依赖库
itextpdf : '5.5.13.2', // PDF文件生成的依赖库
easypoi : '4.3.0', // 生成Excel处理的依赖库
hibernateValidator : '6.2.0.Final', // JSR303验证库
prometheus : '1.6.5', // Prometheus监控数据版本
shedlock : '4.23.0', // ShedLock组件
springDataRedis : '2.4.5', // SpringDataRedis版本
commonsPool2 : '2.9.0', // 连接池版本
]
ext.libraries = [ // 定义所有的依赖库
// 以下的配置为SpringBoot项目所需要的核心依赖
'spring-boot-gradle-plugin': "org.springframework.boot:spring-boot-gradle-plugin:${versions.springboot}",
// 以下的配置为与项目用例测试有关的依赖
'junit-jupiter-api': "org.junit.jupiter:junit-jupiter-api:${versions.junit}",
'junit-vintage-engine': "org.junit.vintage:junit-vintage-engine:${versions.junit}",
'junit-jupiter-engine': "org.junit.jupiter:junit-jupiter-engine:${versions.junit}",
'junit-platform-launcher': "org.junit.platform:junit-platform-launcher:${versions.junitPlatformLauncher}",
'junit-bom': "org.junit:junit-bom:${versions.junit}",
// 以下的配置为Lombok组件有关的依赖
'lombok': "org.projectlombok:lombok:${versions.lombok}",
// 以下的配置为FastJSON组件有关的依赖
'fastjson': "com.alibaba:fastjson:${versions.fastjson}",
// 以下的配置为Jackson将输出转换为XML有关的依赖
'jackson-dataformat-xml': "com.fasterxml.jackson.dataformat:jackson-dataformat-xml:${versions.jackson}",
'jackson-databind': "com.fasterxml.jackson.core:jackson-databind:${versions.jackson}",
'jackson-annotations': "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}",
// 以下的配置为ITextPDF输出的有关依赖配置
'itextpdf': "com.itextpdf:itextpdf:${versions.itextpdf}",
// 以下的配置为生成Excel文件有关的依赖配置
'easypoi-spring-boot-starter': "cn.afterturn:easypoi-spring-boot-starter:${versions.easypoi}",
// 以下的配置为HibernateValidator实现的JSR303验证标准依赖
'hibernate-validator': "org.hibernate.validator:hibernate-validator:${versions.hibernateValidator}",
// 以下的配置为Prometheus监控数据操作
'micrometer-registry-prometheus': "io.micrometer:micrometer-registry-prometheus:${versions.prometheus}",
// 以下的配置为ShedLock分布式任务调度组件
'shedlock-spring': "net.javacrumbs.shedlock:shedlock-spring:${versions.shedlock}",
'shedlock-provider-redis-spring': "net.javacrumbs.shedlock:shedlock-provider-redis-spring:${versions.shedlock}",
// 以下的配置为Redis缓存组件
'spring-boot-starter-data-redis': "org.springframework.boot:spring-boot-starter-data-redis:${versions.springDataRedis}",
'commons-pool2': "org.apache.commons:commons-pool2:${versions.commonsPool2}"
]
4、
project('microboot-web') { // 子模块
dependencies { // 配置子模块依赖
compile(project(':microboot-common')) // 引入其他子模块
compile('org.springframework.boot:spring-boot-starter-actuator')
compile(libraries.'micrometer-registry-prometheus')
implementation(libraries.'shedlock-spring')
implementation(libraries.'shedlock-provider-redis-spring')
implementation(libraries.'spring-boot-starter-data-redis')
implementation(libraries.'commons-pool2')
}
}
5、
spring:
profiles:
active: env
redis:
host: redis-single # Redis主机地址
port: 6379 # Redis端口
password: hello # 密码
database: 0 # 第0个数据库
connect-timeout: 200
timeout: 200
lettuce: # 使用Lettuce连接池
pool:
max-active: 100
max-idle: 20
min-idle: 10
max-wait: 1000
time-between-eviction-runs: 2000 # 每2秒回收一次空闲连接
6、
package com.yootk.task;
import lombok.extern.slf4j.Slf4j;
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
@Component // 任务的处理类需要提供有一个组件
@Slf4j // 直接通过日志输出任务信息
public class YootkScheduleTask { // 定义任务处理类
// @SchedulerLock注解里面对于任务独占锁的时间有两个配置项:
// lockAtLeastFor:成功执行定时任务时任务节点所能拥有独占锁的最短时间;
// lockAtMostFor:成功执行定时任务时任务节点所能拥有独占锁的最长时间;
@Scheduled(cron = "*/2 * * * * ?") // 每2秒触发一次任务
@SchedulerLock(name = "yootk-task", lockAtLeastFor = "5000")
public void task() { // CRON任务
log.info("【ShedLock任务】{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()));
}
}
7、
package com.yootk.task;
import lombok.extern.slf4j.Slf4j;
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Component // 任务的处理类需要提供有一个组件
@Slf4j // 直接通过日志输出任务信息
public class YootkScheduleTask { // 定义任务处理类
// @SchedulerLock注解里面对于任务独占锁的时间有两个配置项:
// lockAtLeastFor:成功执行定时任务时任务节点所能拥有独占锁的最短时间;
// lockAtMostFor:成功执行定时任务时任务节点所能拥有独占锁的最长时间;
@Scheduled(cron = "*/2 * * * * ?") // 每2秒触发一次任务
@SchedulerLock(name = "yootk-task", lockAtLeastFor = "5000")
public void task() throws Exception { // CRON任务
log.info("【ShedLock任务】{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()));
TimeUnit.SECONDS.sleep(5000);
}
}
8、
package com.yootk.config;
import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider;
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration // 配置类注解
@EnableScheduling // 启用定时任务
// 分布式任务调度如果锁被强制霸占,那么其他节点的任务是无法访问的
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S") // 30秒强制释放锁
public class ShedLockRedisConfig {
@Value("${spring.profiles.active}") // 采用默认的环境
private String env; // 当前应用环境
@Bean
public LockProvider lockProvider(RedisConnectionFactory connectionFactory) {
return new RedisLockProvider(connectionFactory, this.env);
}
}
ShedLock 动态任务管理
动态任务管理
- 每一个项目随着时间的推移,都有可能会出现有定时任务触发表达式变更的需求,管理员可以根据实际的项目运行环境,动态的修改定时任务的配置表达式

动态表达式管理
- 动态任务管理的核心在于所有的任务需要通过 SchedulingConfiqurer 接口实例进行动态的配置,在配置时可以明确的定义要执行的任务处理方法以及任务触发表达式(本次基于 CRON 表达式管理),同时为了便于触发表达式的管理,可以将其设置到专属的数据存储空间(例如:SQL 数据库)之中,这样管理员只需要对 CRON 表达式的内容修改后就可以自动的实现任务的配置管理

1、
package com.yootk.task;
import lombok.Data;
import org.springframework.stereotype.Component;
@Component // 是一个独立的组件
@Data // 生成相关的类结构
public class DynmaicCronExpression { // 动态的CRON表达式
private String cron = "*/2 * * * * ?"; // 此时定义CRON表达式
}
2、
package com.yootk.task;
import lombok.extern.slf4j.Slf4j;
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Component // 任务的处理类需要提供有一个组件
@Slf4j // 直接通过日志输出任务信息
public class YootkScheduleTask { // 定义任务处理类
@SchedulerLock(name = "yootk-task", lockAtLeastFor = "5000")
public void task() { // CRON任务
log.info("【ShedLock任务】{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()));
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3、
package com.yootk.config;
import com.yootk.task.DynmaicCronExpression;
import com.yootk.task.YootkScheduleTask;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
@Configuration
@Slf4j // 在每次修改的时候实现一些输出
public class DynmaicScheduleConfig implements SchedulingConfigurer {// 动态配置
@Autowired // 需要额外注入
private DynmaicCronExpression expression; // 动态表达式
@Autowired // 额外注入
private YootkScheduleTask task; // 定时任务处理类
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addTriggerTask(
() -> task.task(), // 当前要执行的任务
triggerContext -> { // 设置任务的触发表达式
log.info("设置当前的CRON表达式:{}", expression.getCron()); // 日志输出
String cron = expression.getCron(); // 获取当前已经设置的CRON表达式
return new CronTrigger((cron)).nextExecutionTime(triggerContext);
}
); // 设置处罚任务
}
}
4、
package com.yootk.action;
import com.yootk.task.DynmaicCronExpression;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/cron/*")
@Slf4j
public class CronAction {
@Autowired
private DynmaicCronExpression expression; // 动态表达式
@GetMapping("set")
public Object setCron(String cron) {
log.info("动态修改CRON配置:{}", cron);
this.expression.setCron(cron);// 修改当前触发的CRON表达式
return true;
}
}
5、
localhost/cron/set?cron=*/15 * * * * ?
自定义事件
自定义事件概述
事件实现业务解耦和
- 在完善的项目分层设计结构之中,常规性的做法都是将项目中所需要的大量业务处理逻辑直接定义在业务层之中,这样一来随着业务的不断完善,业务层中对应的程序代码组成也将越来越多,例如:当用户注册完成后需要向其对应的注册邮箱或者手机中发送验证的信息,而如果将这类操作放在业务层中,那么就会造成代码结构上的混乱,毕竟这些都只是辅助功能,最佳的做法就是让其发布一个用户注册的事件,而后具体的信息发送由事件的处理类去完成,这样就可以成功的实现业务解耦和操作
自定义事件处理
Spring 自定义事件
- Spring 中的事件处理实际上是针对于已有 Java 事件机制的一种延续,为了便于 Spring 实现事件操作,在已有的 EventObject 事件类的基础上扩充了 ApplicationEvent 抽象事件类,同时又在 EventListener 监听接口基础上扩充了 ApplicationListener 子接口,开发者如果要进行事件的监听注册,只需要将 ApplicationListener 接口子类实例进行 Bean 定义,则会自动在 Spring 容器中进行注册,所有的事件注册都是通过 ApplicationEventPublisher 接口实现,这样在产生了指定类型的事件对象实例后就可以自动的匹配事件监听并进行处理

1、
package com.yootk.vo;
import lombok.Data;
@Data // 只要使用Lombok组件,99%的情况下就使用这一个注解
public class Message {
private String title; // 字符串的参数
private String url;
}
2、
package com.yootk.event;
import com.yootk.vo.Message;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEvent;
@Getter
@Slf4j
public class YootkEvent extends ApplicationEvent { // 自定义事件类
private Message message; // 数据保存
public YootkEvent(Object source, Message message) { // 产生事件之后保存有具体的事件数据
super(source);
this.message = message; // 数据存储
}
public void fire() { // 自定义方法
log.info("message = {}", this.message); // 日志输出
}
}
3、
package com.yootk.event.listener;
import com.yootk.event.YootkEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class YootkListener implements ApplicationListener<YootkEvent> { // 事件绑定
@Override
public void onApplicationEvent(YootkEvent event) { // 事件监听后得到事件对象
log.info("事件处理:{}", event);
event.fire(); // 自定义的事件操作
}
}
4、
package com.yootk.test;
import com.yootk.StartSpringBootApplication;
import com.yootk.event.YootkEvent;
import com.yootk.vo.Message;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
@ExtendWith(SpringExtension.class) // 使用JUnit5测试工具
@WebAppConfiguration // 启动WEB运行环境
@SpringBootTest(classes = StartSpringBootApplication.class) // 配置程序启动类
public class TestEvent { // 编写测试类
@Autowired // 是由Spring容器提供的
private ApplicationEventPublisher publisher; // 事件发布类
@Test
public void testEvent() { // 进行响应测试
Message message = new Message(); // 实例化Message对象
message.setTitle("沐言科技");
message.setUrl("www.yootk.com");
this.publisher.publishEvent(new YootkEvent(this, message));
}
}
@EventListener 注解
自定义监听处理方法
- 传统的事件监听处理往往都需要配置有一个专属的监听程序类,同时在该类中必须明确的实现 ApplicationListener 父接口,这样对于程序开发的灵活性就有了一定的限制,为了进一步帮助开发者简化事件监听的处理模型,在 Spring 中可以直接通过一个配置类并通过“@EventListener”注解直接定义事件监听处理方法,同时多个监听操作方法还可以自动的根据传递参数的类型判断执行

1、
package com.yootk.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
@Configuration // 配置类
@Slf4j
public class EventListenerConfig { // 事件监听配置类
@EventListener // 事件监听,没有了强制性的接口实现
public void handleAllEvent(Object event) {
log.info("【handleAllEvent()】{}", event); // 监听所有的事件
}
}
2、
package com.yootk.test;
import com.yootk.StartSpringBootApplication;
import com.yootk.event.YootkEvent;
import com.yootk.vo.Message;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
@ExtendWith(SpringExtension.class) // 使用JUnit5测试工具
@WebAppConfiguration // 启动WEB运行环境
@SpringBootTest(classes = StartSpringBootApplication.class) // 配置程序启动类
public class TestEvent { // 编写测试类
@Autowired // 是由Spring容器提供的
private ApplicationEventPublisher publisher; // 事件发布类
@Test
public void testEvent() { // 进行响应测试
}
}
3、
package com.yootk.config;
import com.yootk.event.YootkEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
@Configuration // 配置类
@Slf4j
public class EventListenerConfig { // 事件监听配置类
// @EventListener // 事件监听,没有了强制性的接口实现
public void handleAllEvent(Object event) {
log.info("【handleAllEvent()】{}", event); // 监听所有的事件
}
@EventListener
public void handleYootkEvent(YootkEvent event) { // 绑定指定的事件类型
log.info("【handleYootkEvent()】{}", event); // 只要有此Event事件都会进行监听
}
}
4、
package com.yootk.test;
import com.yootk.StartSpringBootApplication;
import com.yootk.event.YootkEvent;
import com.yootk.vo.Message;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
@ExtendWith(SpringExtension.class) // 使用JUnit5测试工具
@WebAppConfiguration // 启动WEB运行环境
@SpringBootTest(classes = StartSpringBootApplication.class) // 配置程序启动类
public class TestEvent { // 编写测试类
@Autowired // 是由Spring容器提供的
private ApplicationEventPublisher publisher; // 事件发布类
@Test
public void testEvent() { // 进行响应测试
this.publisher.publishEvent(new YootkEvent(this, new Message("沐言科技", "www.yootk.com")));
}
}
5、
package com.yootk.config;
import com.yootk.event.YootkEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
@Configuration // 配置类
@Slf4j
public class EventListenerConfig { // 事件监听配置类
@EventListener(condition = "#event.message.title == 'yootk'") // 条件触发
public void handleYootkEventByCondition(YootkEvent event) { // 绑定指定的事件类型
log.info("【handleYootkEventByCondition()】{}", event); // 只要有此Event事件都会进行监听
}
}
6、
package com.yootk.test;
import com.yootk.StartSpringBootApplication;
import com.yootk.event.YootkEvent;
import com.yootk.vo.Message;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
@ExtendWith(SpringExtension.class) // 使用JUnit5测试工具
@WebAppConfiguration // 启动WEB运行环境
@SpringBootTest(classes = StartSpringBootApplication.class) // 配置程序启动类
public class TestEvent { // 编写测试类
@Autowired // 是由Spring容器提供的
private ApplicationEventPublisher publisher; // 事件发布类
@Test
public void testEvent() { // 进行响应测试
this.publisher.publishEvent(new YootkEvent(this, new Message("yootk", "www.yootk.com")));
}
}
7、
package com.yootk.config;
import com.yootk.vo.Message;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
@Configuration // 配置类
@Slf4j
public class EventListenerConfig { // 事件监听配置类
@EventListener // 条件触发
public void handleYootkEventByContent(Message message) {
log.info("【handleYootkEventByContent()】{}", message);
}
}
8、
package com.yootk.test;
import com.yootk.StartSpringBootApplication;
import com.yootk.vo.Message;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
@ExtendWith(SpringExtension.class) // 使用JUnit5测试工具
@WebAppConfiguration // 启动WEB运行环境
@SpringBootTest(classes = StartSpringBootApplication.class) // 配置程序启动类
public class TestEvent { // 编写测试类
@Autowired // 是由Spring容器提供的
private ApplicationEventPublisher publisher; // 事件发布类
@Test
public void testEvent() { // 进行响应测试
this.publisher.publishEvent(new Message("yootk", "www.yootk.com"));
}
}
WebService
WebService 简介
WebService 技术架构
- WebService 是一种传统的 SOA 技术架构,它不依赖于任何的编程语言,也不依赖于任何的技术平台,可以直接基于 HTTP 协议实现网络应用间的数据交互

- WebService 组成结构依然采用了传统的“C/S”模型,如果某个平台需要对外暴露操作接口,这个时候就可以直接通过 WSDL(Web Services Description Language.Web 服务描述语言)对要公布的接口进行描述
WebService 项目结构
- 在本次所讲解的 WebService 程序实现将基于 JDK11 实现,为便于程序结构的管理,将采用图所示的结构进行开发,将项目分为三个模块:公共组件,服务端,客户端

1、
// https://mvnrepository.com/artifact/com.sun.xml.ws/jaxws-ri
implementation group: 'com.sun.xml.ws', name: 'jaxws-ri', version: '2.3.4', ext: 'pom'
implementation group: 'com.sun.xml.ws', name: 'jaxws-ri', version: '4.0.2', ext: 'pom'
// https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator
implementation group: 'org.hibernate.validator', name: 'hibernate-validator', version: '6.2.0.Final'
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web-services
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web-services', version: '2.4.5'
// https://mvnrepository.com/artifact/org.apache.cxf/cxf-spring-boot-starter-jaxws
implementation group: 'org.apache.cxf', name: 'cxf-spring-boot-starter-jaxws', version: '3.4.3'
// https://mvnrepository.com/artifact/org.apache.cxf/cxf-rt-transports-http
implementation group: 'org.apache.cxf', name: 'cxf-rt-transports-http', version: '3.4.3'
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.4.5'
2、
ext.versions = [ // 定义所有要使用的版本号
springboot : '2.4.3', // SpringBoot版本号
junit : '5.7.1', // 配置JUnit测试工具的版本编号
junitPlatformLauncher : '1.7.1', // JUnit测试工具运行平台版本编号
lombok : '1.18.18', // Lombok插件对应的版本号
fastjson : '1.2.75', // FastJSON组件对应的版本号
jackson : '2.12.2', // 配置Jackson相关依赖库
itextpdf : '5.5.13.2', // PDF文件生成的依赖库
easypoi : '4.3.0', // 生成Excel处理的依赖库
hibernateValidator : '6.2.0.Final', // JSR303验证库
prometheus : '1.6.5', // Prometheus监控数据版本
shedlock : '4.23.0', // ShedLock组件
springDataRedis : '2.4.5', // SpringDataRedis版本
commonsPool2 : '2.9.0', // 连接池版本
jaxwsRi : '2.3.4', // JDK-WS依赖
cxf : '3.4.3', // WEBService开发框架版本
]
ext.libraries = [ // 定义所有的依赖库
// 以下的配置为SpringBoot项目所需要的核心依赖
'spring-boot-gradle-plugin': "org.springframework.boot:spring-boot-gradle-plugin:${versions.springboot}",
// 以下的配置为与项目用例测试有关的依赖
'junit-jupiter-api': "org.junit.jupiter:junit-jupiter-api:${versions.junit}",
'junit-vintage-engine': "org.junit.vintage:junit-vintage-engine:${versions.junit}",
'junit-jupiter-engine': "org.junit.jupiter:junit-jupiter-engine:${versions.junit}",
'junit-platform-launcher': "org.junit.platform:junit-platform-launcher:${versions.junitPlatformLauncher}",
'junit-bom': "org.junit:junit-bom:${versions.junit}",
// 以下的配置为Lombok组件有关的依赖
'lombok': "org.projectlombok:lombok:${versions.lombok}",
// 以下的配置为FastJSON组件有关的依赖
'fastjson': "com.alibaba:fastjson:${versions.fastjson}",
// 以下的配置为Jackson将输出转换为XML有关的依赖
'jackson-dataformat-xml': "com.fasterxml.jackson.dataformat:jackson-dataformat-xml:${versions.jackson}",
'jackson-databind': "com.fasterxml.jackson.core:jackson-databind:${versions.jackson}",
'jackson-annotations': "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}",
// 以下的配置为ITextPDF输出的有关依赖配置
'itextpdf': "com.itextpdf:itextpdf:${versions.itextpdf}",
// 以下的配置为生成Excel文件有关的依赖配置
'easypoi-spring-boot-starter': "cn.afterturn:easypoi-spring-boot-starter:${versions.easypoi}",
// 以下的配置为HibernateValidator实现的JSR303验证标准依赖
'hibernate-validator': "org.hibernate.validator:hibernate-validator:${versions.hibernateValidator}",
// 以下的配置为Prometheus监控数据操作
'micrometer-registry-prometheus': "io.micrometer:micrometer-registry-prometheus:${versions.prometheus}",
// 以下的配置为ShedLock分布式任务调度组件
'shedlock-spring': "net.javacrumbs.shedlock:shedlock-spring:${versions.shedlock}",
'shedlock-provider-redis-spring': "net.javacrumbs.shedlock:shedlock-provider-redis-spring:${versions.shedlock}",
// 以下的配置为Redis缓存组件
'spring-boot-starter-data-redis': "org.springframework.boot:spring-boot-starter-data-redis:${versions.springDataRedis}",
'commons-pool2': "org.apache.commons:commons-pool2:${versions.commonsPool2}",
// 以下的配置为WebService开发所需要的依赖:
'jaxws-ri': "com.sun.xml.ws:jaxws-ri:${versions.jaxwsRi}",
'cxf-spring-boot-starter-jaxws': "org.apache.cxf:cxf-spring-boot-starter-jaxws:${versions.cxf}",
'cxf-rt-transports-http': "org.apache.cxf:cxf-rt-transports-http:${versions.cxf}",
]
3、
project('microboot-webservice-common') { // 子模块
dependencies { // 配置子模块依赖
compile('org.springframework.boot:spring-boot-starter-web')// 引入SpringBoot依赖
compile('org.springframework.boot:spring-boot-starter-web-services') // WebService依赖
compile(libraries.'jaxws-ri')
compile(libraries.'cxf-spring-boot-starter-jaxws')
compile(libraries.'cxf-rt-transports-http')
compile(libraries.'hibernate-validator')
}
}
project('microboot-webservice-server') {
dependencies { // 配置子模块依赖
compile(project(':microboot-webservice-common')) // 引入其他子模块
}
}
project('microboot-webservice-client') {
dependencies { // 配置子模块依赖
compile(project(':microboot-webservice-common')) // 引入其他子模块
}
}
4、
jar {enabled = true} // 允许当前的模块打包为“*.jar”文件
javadocTask {enabled = false} // 不启用javadoc任务
javadocJar {enabled = false} // 不生成javadoc的“*.jar”文件
bootJar {enabled = false} // 不执行SpringBoot的打包命令
搭建 WebService 服务端
WebService 服务开发
- WebService 的程序开发中最为重要的一个组成结构就是服务接口的描述开发者可以直接利用 JWS 所提供的注解定义接口名称、处理方法以及处理参数的定义随后就可以利用 CXF 框架所提供的工具类进行服务注册以及访问路径的配置

1、
package com.yootk.service;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
@WebService(name = "MessageService", targetNamespace = "http://service.yootk.com/") // 与包名称相反
public interface IMessageService {
@WebMethod // 进行WebService方法标注
public String echo(@WebParam String msg);
}
2、
gradle build
3、
package com.yootk.service.impl;
import com.yootk.service.IMessageService;
import org.springframework.stereotype.Service;
import javax.jws.WebService;
@WebService(serviceName = "MessageService",
targetNamespace = "http://service.yootk.com/", // 接口命名空间
endpointInterface = "com.yootk.service.IMessageService") // 接口的名称
@Service // 自动进行Bean注册
public class MessageServiceImpl implements IMessageService {
@Override
public String echo(String msg) {
return "【ECHO】" + msg;
}
}
4、
package com.yootk.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.binding.soap.saaj.SAAJInInterceptor;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;
import org.springframework.stereotype.Component;
import org.w3c.dom.NodeList;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPMessage;
@Component
@Slf4j
public class WebServiceAuthInterceptor extends AbstractPhaseInterceptor<SoapMessage> {
private static final String USER_NAME = "muyan"; // 用户名
private static final String USER_PASSWORD = "yootk.com"; // 密码
private SAAJInInterceptor saa = new SAAJInInterceptor(); // 创建拦截器
public WebServiceAuthInterceptor() {
super(Phase.PRE_PROTOCOL);
super.getAfter().add(SAAJInInterceptor.class.getName()); // 添加拦截
}
@Override
public void handleMessage(SoapMessage message) throws Fault {
SOAPMessage soapMessage = message.getContent(SOAPMessage.class); // 获取指定消息
if (soapMessage == null) { // 没有消息内容
this.saa.handleMessage(message); // 直接走默认处理
soapMessage = message.getContent(SOAPMessage.class); // 尝试获取消息
}
SOAPHeader header = null; // SOAP头信息
try {
header = soapMessage.getSOAPHeader(); // 通过消息获取头信息
} catch (SOAPException e) {
e.printStackTrace();
}
if (header == null) { // 没有头信息
throw new Fault(new IllegalAccessException("找不到Header信息,无法实现用户认证处理!"));
}
// SOAP是基于XML文件结构进行传输的,所以如果要想获取认证信息就必须进行相关的结构约定
NodeList usernameNodeList = header.getElementsByTagName("username"); // 获取指定标签集合
NodeList passwordNodeList = header.getElementsByTagName("password"); // 获取DOM数据
if (usernameNodeList.getLength() < 1) {
throw new Fault(new IllegalAccessException("找不到Header信息,无法实现用户认证处理!"));
}
if (passwordNodeList.getLength() < 1) {
throw new Fault(new IllegalAccessException("找不到Header信息,无法实现用户认证处理!"));
}
String username = usernameNodeList.item(0).getTextContent().trim() ; // 获取用户名
String password = passwordNodeList.item(0).getTextContent().trim() ; // 获取密码
if (USER_NAME.equals(username) && USER_PASSWORD.equals(password)) { // 认证信息合法
log.debug("用户访问认证成功!");
} else { // 用户认证失败
SOAPException soapException = new SOAPException("用户认证失败!"); // 抛出异常
log.debug("用户认证失败!");
throw new Fault(soapException);
}
}
}
5、
package com.yootk.config;
import com.yootk.interceptor.WebServiceAuthInterceptor;
import com.yootk.service.IMessageService;
import org.apache.cxf.Bus;
import org.apache.cxf.jaxws.EndpointImpl;
import org.apache.cxf.transport.servlet.CXFServlet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.xml.ws.Endpoint;
@Configuration
public class CXFConfig {
@Autowired
private Bus bus; // 注入Bus接口实例
@Autowired
private IMessageService messageService; // 注入业务实例
@Autowired
private WebServiceAuthInterceptor interceptor; // 认证拦截器
@Bean
public ServletRegistrationBean getRegistrationBean() {
return new ServletRegistrationBean(new CXFServlet(), "/services/*"); // WebService访问的父路径
}
@Bean
public Endpoint messageEndPoint() {
EndpointImpl endpoint = new EndpointImpl(this.bus, this.messageService);
endpoint.publish("/MessageService");
endpoint.getInInterceptors().add(this.interceptor); // 访问拦截器
return endpoint;
}
}
6、
package com.yootk;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StartSpringBootWebService {
public static void main(String[] args) {
SpringApplication.run(StartSpringBootWebService.class, args);
}
}
开发 WebService 客户端
传统 WebService 服务调用
- 在传统的 WebService 客户端调用过程之中,一般都需要依据接口所提供的 WSDL 描述文件定义结构,在本地生成相应的服务接口以及相关伪代码

WebService 客户端访问
- 使用了 CXF 之后就可以直接利用框架中所提供的程序类直接根据 WSDL 访问地址实现服务调用,由于服务端已经添加了认证管理操作,这样在客户端传递了正确的认证信息后才可以进行接口调用

1、
package com.yootk.ws.util;
import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.headers.Header;
import org.apache.cxf.helpers.DOMUtils;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.xml.namespace.QName;
import java.util.List;
public class ClientLoginInterceptor extends AbstractPhaseInterceptor<SoapMessage> { // 客户端登录拦截器
private String username; // 用户名
private String password; // 密码
public ClientLoginInterceptor(String username, String password) {
super(Phase.PREPARE_SEND);
this.username = username;
this.password = password;
}
@Override
public void handleMessage(SoapMessage soap) throws Fault {
List<Header> headers = soap.getHeaders(); // 获取全部头信息
Document doc = DOMUtils.createDocument(); // 创建文档
Element auth = doc.createElement("authority"); // 认证数据信息
Element username = doc.createElement("username");
Element password = doc.createElement("password");
username.setTextContent(this.username);
password.setTextContent(this.password);
auth.appendChild(username);
auth.appendChild(password);
headers.add(0, new Header(new QName("authority"), auth));
}
}
2、
http://localhost:8080/services/MessageService?wsdl
3、
package com.yootk.ws.client;
import com.yootk.service.IMessageService;
import com.yootk.ws.util.ClientLoginInterceptor;
import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
public class CXFClientProxy {
public static void main(String[] args) throws Exception {
String address = "http://localhost:8080/services/MessageService?wsdl"; // WebService服务地址
JaxWsProxyFactoryBean jaxWsProxyFactoryBean = new JaxWsProxyFactoryBean();
jaxWsProxyFactoryBean.setAddress(address); // 代理地址
jaxWsProxyFactoryBean.setServiceClass(IMessageService.class); // 映射的接口
jaxWsProxyFactoryBean.getOutInterceptors().add(
new ClientLoginInterceptor("muyan", "yootk.com")); // 设置认证数据
IMessageService messageService = (IMessageService) jaxWsProxyFactoryBean.create(); // 远程接口映射
String message = "沐言科技:www.yootk.com";
String result = messageService.echo(message); // 业务调用
System.out.println(result);
}
}
4、
package com.yootk.ws.client;
import com.yootk.ws.util.ClientLoginInterceptor;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.jaxws.endpoint.dynamic.JaxWsDynamicClientFactory;
public class CXFClientDynamic {
public static void main(String[] args) throws Exception {
String address = "http://localhost:8080/services/MessageService?wsdl"; // WebService服务地址
JaxWsDynamicClientFactory dcf = JaxWsDynamicClientFactory.newInstance();
Client client = dcf.createClient(address); // 创建一个客户端类
client.getOutInterceptors().add(new ClientLoginInterceptor("muyan", "yootk.com"));
String message = "沐言科技:www.yootk.com";
Object[] result = client.invoke("echo", message);
System.out.println(result[0]);
}
}
WebSocket
WebSocket 简介
Ajax 轮询
- 在 WEB 开发中 AJAX 异步数据加载是很多项目都会使用到的技术,而在每次使用 AJAX 进行异步请求处理时,都需要与服务器端建立一个新的请求连接,而后在每次请求处理完成后服务器端也会自动的进行该连接的关闭,这样一来如果在频繁的 AJAX 数据交互过程中就会产生严重的性能问题

WebSocket 通讯
- HTML5 的技术的推出,在 WEB 开发中又提供了一种新的 WebSocket 通讯技术,使用该技术可以在单个 TCP 连接中实现全双工通讯协议,同时客户端与服务器端之间只需要建立一次握手连接,就可以创建持久性的连接,并实现双向实时数据传输的功能,相比较 AJAX 的轮询模式,WebSocket 的通讯形式可以更好的节约服务器资源和网络带宽。

开发 WebSocket 服务端
WebSocket 处理机制
- WebSocket 是基于事件方式实现的通讯操作,所以在实现中需要创建有一个专属的 WebSocket 处理类,而后分别定义连接处理方法(使用“@OnOpen”注解)通讯处理方法(使用“@OnMessage”注解)、错误处理方法(使用“@OnError”注解)以及关闭处理方法(使用“@OnClose”注解),这样当用户请求发送后就可以根据不同的请求状态进行处理

1、
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-websocket
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-websocket', version: '2.4.5'
2、
project('microboot-web') { // 子模块
dependencies { // 配置子模块依赖
compile(project(':microboot-common')) // 引入其他子模块
compile('org.springframework.boot:spring-boot-starter-actuator')
compile('org.springframework.boot:spring-boot-starter-websocket')
}
}
3、
package com.yootk.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration // 配置类注解
@EnableWebSocket // 启用WebSocket服务
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
4、
package com.yootk.websocket;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.UUID;
@ServerEndpoint("/websocket/{token}") // 需要提供给客户端一个操作终端
@Component
@Slf4j
public class WebSocketHandler { // 定义WebSocket处理类
@OnOpen // 只有追加此注解之后才表示开启响应的事件监听
public void openHandler(
Session session, // 实现客户端的身份保存
@PathParam("token") String token // 此时存在有一个路径的参数
) { // 进行连接的开启事件处理
// 本次的操作是基于token模拟了一次授权操作,只是一个假的
if (token == null || "".equals(token)) { // 象征性的做一个检查
this.sendMessage(session, "【ERROR】客户端Token错误,连接失败!");
}
log.info("客户端创建WebSocket连接,SessionId = {}", session.getId()); // 日志输出
// 考虑到后续用户的访问情况,可以继续随意发送一个Token数据,本次就直接通过UUID模拟了
this.sendMessage(session, UUID.randomUUID().toString()); // 假装有了一个随机的Token
}
@OnClose // 关闭WebSocket连接通道
public void closeHandler(Session session) {
log.info("客户端断开WebSocket连接,SessionID = {}", session.getId());
}
@OnError // 错误时处理
public void errorHandler(Session session, Throwable throwable) {
log.info("程序出现了错误:{}", throwable);
}
@OnMessage // 消息处理
public void messageHandler(Session session, String message) {
log.info("【{}】用户发送请求,message内容为:{}", session.getId(), message);
this.sendMessage(session, "【ECHO】" + message);
}
// 不管是打开连接还是关闭连接,以及最终需要进行数据的交互那么都要进行数据的发送处理
private void sendMessage(Session session, String message) {
if (session != null) { // 判断当前的session是否存在
synchronized (session) { // 做一个同步
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {}
}
}
}
}
开发 WebSocket 客户端
1、
<!DOCTYPE HTML>
<head>
<title>WebSocket交互</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> <!-- 页面编码 -->
<link rel="icon" type="image/x-icon" href="images/favicon.ico"/> <!-- 页面ICON -->
<meta name="viewport" content="width=device-width,initial-scale=1">
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" src="bootstrap/js/bootstrap.min.js"></script>
<link rel="stylesheet" type="text/css" href="bootstrap/css/bootstrap.min.css"/>
<script type="text/javascript"> // WebSocket是需要通过JS编写的
url = "ws://localhost/websocket/yootk-token"; // 此时采用的是一个默认的token标记
window.onload = function() { // 本次不使用开发框架,直接采用原生的JS进行开发
webSocket = new WebSocket(url); // 获取WebSocket类的实例化对象
webSocket.onopen = function(dev) { // 进行连接的开启
document.getElementById("messageDiv").innerHTML +=
"<p>服务器连接成功,开始进行消息的交互处理。</p>"
}
webSocket.onclose = function() {
document.getElementById("messageDiv").innerHTML +=
"<p>消息交互完毕,关闭连接通道。</p>"
}
document.getElementById("send").addEventListener("click", function() {
inputMessage = document.getElementById("msg").value; // 获取文本输入内容
webSocket.send(inputMessage); // 进行消息的发送
webSocket.onmessage = function(obj) { // 消息的回应处理
document.getElementById("messageDiv").innerHTML +=
"<p>" + obj.data + "</p>"; // 接收内容响应
document.getElementById("msg").value = ""; // 清空文本输入框
}
});
document.getElementById("close").addEventListener("click", function() {
webSocket.close(); // 关闭
})
}
</script>
</head>
<body>
<div> </div>
<div class="row" style="margin: 10px;">
<div class="panel panel-success">
<div class="panel-heading">
<strong><i class="glyphicon glyphicon-th-list"></i> WebSocket数据交互</strong>
</div>
<div class="panel-body">
<div id="inputDiv">
<form class="form-horizontal" id="messageform">
<div class="form-group" id="midDiv">
<label class="col-md-2 control-label">输入信息:</label>
<div class="col-md-8">
<input type="text" id="msg" name="msg" class="form-control" placeholder="请输入交互信息...">
</div>
<div class="col-md-2">
<button type="button" class="btn btn-primary btn-sm" id="send">发送</button>
<button type="button" class="btn btn-danger btn-sm" id="close">关闭</button>
</div>
</div>
</form>
</div>
<div id="messageDiv"></div>
</div>
<div class="panel-footer">
<div style="text-align:right;">
<img src="images/logo.png" style="height: 30px;">
<strong>沐言科技(www.yootk.com) —— 新时代软件教育领导品牌</strong>
</div>
</div>
</div>
</div>
</body>
</html>
demo
