本文介绍的内容可能没有太多的用处,纯属个人爱好和异想天开,主要是想加深一下Spring AOP的认识。
先来看一下结构图
再看一下效果图
接下来,开始开发工作吧
一、定义插件实体类
/**
* 主键
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 名称
*/
@Column
private String name;
/**
* 描述
*/
@Column
private String description;
/**
* 类名
*/
@Column
private String className;
/**
* 文件(jar包)名
*/
@Column
private String fileName;
/**
* 版本
*/
@Column
private String version = "1.0";
/**
* 状态 0:已安装,1:已激活
*/
@Column
private Integer status;
/**
* 创建时间
*/
@Column
private Date createTime = new Date();
/**
* 更新时间
*/
@Column
private Date updateTime;
/**
* 文件内容
*/
@Column
private byte[] content;
二、对应的增删改查方法
因为使用了Spring Date JPA,很简单的就不贴代码了,主要展示一下jar包上传部分。
@PostMapping(API_BASE_PAH + "/add")
public Result add(Plugin plugin, HttpServletRequest request) throws IOException {
// 处理文件
if (request instanceof MultipartHttpServletRequest) {
MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest) request;
// 获取上传的文件
Map<String, MultipartFile> fileMap = multipartHttpServletRequest.getFileMap();
Map.Entry<String, MultipartFile> fileEntry = fileMap.entrySet().stream()
.filter(entry -> "jarFile".equalsIgnoreCase(entry.getKey()))
.findAny()
.orElse(null);
if (fileEntry != null) {
MultipartFile multipartFile = fileEntry.getValue();
plugin.setFileName(multipartFile.getOriginalFilename());
plugin.setContent(multipartFile.getBytes());
}
}
// 保存
Plugin save = service.save(plugin);
return Result.ok("添加成功!", save);
}
有一部分相对比较困难一点的,我们在内存中定义了两个map,记录已安装的插件和已激活的插件。所以,获取所有插件的时候,需要查这两个map以获得插件的状态。
三、定义插件平台管理的接口
插件的jar包存放位置,我们放在用户的目录下。
/** 插件jar包存放目录 */
private static final String BASE_DIR;
/** 设置插件jar包存放目录 */
static {
BASE_DIR = System.getProperty("user.home") + "/plugin_platform/plugins";
Path baseDirPath = Paths.get(BASE_DIR);
if (Files.notExists(baseDirPath)) {
try {
Files.createDirectories(baseDirPath);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
1、安装插件
首先检查已安装插件Map里面有没有(避免重复安装),然后从数据库获取插件,将jar包的二进制转为文件存到本地(需要处理已存在的情况和删不掉的情况),接着将jar包加载到JVM中,最后检查插件的类是不是符合我们定义的规范(实现了aop的Advice接口),并放进已安装插件Map中。
/**
* 安装插件
* @param id
*/
public void installPlugin(Long id) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException {
// 是否已安装
if (installPlugins.containsKey(id)) {
throw new BusinessException(String.format("已存在指定的插件!"));
}
// 从数据库取出插件
Plugin plugin = service.getById(id);
if (plugin == null) {
throw new BusinessException("插件不存在!");
}
// jar包下载到本地
String fileName = plugin.getFileName();
byte[] fileContent = plugin.getContent();
Path jarPath = Paths.get(BASE_DIR, "/" + fileName);
// 存在就先删除,删不掉就重新命名
try {
Files.deleteIfExists(jarPath);
} catch (Exception e) {
e.printStackTrace();
Long milliSecond = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli();
jarPath = Paths.get(BASE_DIR, "/" + milliSecond + "_" + fileName);
}
InputStream in = new ByteArrayInputStream(fileContent);
Files.copy(in, jarPath);
// 加载(不连接)
URLClassLoader loader = (URLClassLoader) getClass().getClassLoader();
URL targetUrl = jarPath.toUri().toURL();
boolean isLoader = Arrays.stream(loader.getURLs()).anyMatch(url ->
url.equals(targetUrl)
);
if (!isLoader) {
Method add = URLClassLoader.class.getDeclaredMethod("addURL", new Class[] {URL.class});
// 暴力访问
add.setAccessible(true);
add.invoke(loader, targetUrl);
}
// 判断类型是否正确
String className = plugin.getClassName();
Class<?> adviceClass = loader.loadClass(className);
if (!Advice.class.isAssignableFrom(adviceClass)) {
throw new BusinessException(
String.format("配置错误,%s非%s的实现类!", className, Advice.class)
);
}
// 放进缓存
installPlugins.put(id, plugin);
}
2、选择插件切入的代理类并激活插件
获取所有代理类(用于页面选择)。
/**
* 获取IOC容器中所有代理类
* @return
*/
public List<String> getAdvised() {
// 获取所有bean的类名集合
return Arrays.stream(ctx.getBeanDefinitionNames())
.map(ctx::getBean)
// 只要AOP代理的bean
.filter(AopUtils::isAopProxy)
// 获取代理对象的类名
.map(bean -> {
return bean.getClass().getName();
/*Object target = null;
try {
if(AopUtils.isJdkDynamicProxy(bean)) {
target = getJdkDynamicProxyTargetObject(bean);
} else { //cglib
target = getCglibProxyTargetObject(bean);
}
} catch (Exception e) {
e.printStackTrace();
}
return target == null ? "" : target.getClass().getName();*/
})
// 只要自定义类
.filter(className -> className.contains("cn.zhh"))
.collect(Collectors.toList());
}
激活插件
首先判断是否已激活、未安装,然后将插件类注入IOC容器,接着获取所有未添加此通知的代理bean,最后对这些bean添加advice。
/**
* 将插件类注入IOC容器
* @param className
* @return
* @throws ClassNotFoundException
* @throws InterruptedException
*/
private Advice buildAdvice(String className) throws ClassNotFoundException, InterruptedException {
Object bean = registerBean(className);
return (Advice) bean;
}
/**
* 激活插件
* @param id
*/
public void activePlugin(Long id, String[] classNameArray) throws InterruptedException, ClassNotFoundException {
if (activePlugins.containsKey(id)) {
throw new BusinessException("插件已激活!");
}
if (!installPlugins.containsKey(id)) {
throw new BusinessException("插件未安装,请先安装!");
}
Plugin plugin = installPlugins.get(id);
Advice advice = buildAdvice(plugin.getClassName());
// 获取所有的IOC Bean
Arrays.stream(applicationContext.getBeanDefinitionNames())
.map(applicationContext::getBean)
// 过滤掉当前bean和非代理bean
.filter(bean -> bean != this && bean instanceof Advised)
// 过滤掉非选中的bean
.filter(bean -> Arrays.stream(classNameArray).anyMatch(
className -> bean.getClass().getName().equalsIgnoreCase(className)
))
// 过滤掉已添加此通知的bean
.filter(bean -> {
boolean noneMatch = Arrays.stream(((Advised) bean).getAdvisors())
.map(advisor -> advisor.getAdvice().getClass().getName())
.noneMatch(adviceName -> adviceName.equalsIgnoreCase(plugin.getClassName()));
return noneMatch;})
// 添加通知
.forEach(bean ->{
((Advised)bean).addAdvice(advice);
});
activePlugins.put(id, plugin);
}
3、禁用插件
对符合的代理bean移除Advice,并将此通知从IOC容器移除。
/**
* 禁用插件
* @param id
*/
public void disablePlugin(Long id) throws InterruptedException, ClassNotFoundException {
if (!activePlugins.containsKey(id)) {
throw new BusinessException("插件未激活!");
}
Plugin advice = activePlugins.get(id);
// 获取所有的IOC Bean
String className = advice.getClassName();
Arrays.stream(applicationContext.getBeanDefinitionNames())
.map(applicationContext::getBean)
// 过滤掉当前bean
.filter(bean -> bean != this)
// 过滤到非代理bean
.filter(bean -> bean instanceof Advised)
// 禁用通知
.forEach(bean ->{
Advised advised = (Advised) bean;
Arrays.stream(advised.getAdvisors())
.map(advisor -> advisor.getAdvice())
.filter(_advice -> _advice.getClass().getName().equalsIgnoreCase(className))
.forEach(advised::removeAdvice);
});
// 将bean从IOC删除
removeBeanByClass(className);
activePlugins.remove(id);
}
/**
* 根据类名从IOC容器bean, 属于该类型的将全部删除
* @param className
*/
public void removeBeanByClass(String className) throws ClassNotFoundException, InterruptedException {
Class<?> beanClazz = getClass().getClassLoader().loadClass(className);
// 获取bean工厂
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory)ctx.getAutowireCapableBeanFactory();
// 获取所有bean的名称,并转为流
Arrays.stream(ctx.getBeanDefinitionNames())
// 过滤掉不属于指定类型的bean
.filter(definitionName -> {
Object bean = ctx.getBean(definitionName);
return beanClazz.isAssignableFrom(bean.getClass());
})
// 删除bean
.forEach(defaultListableBeanFactory::removeBeanDefinition);
}
4、卸载插件
将插件从已安装插件Map移除即可。
/**
* 卸载插件
* @param id
*/
public void uninstallPlugin(Long id) {
if (!installPlugins.containsKey(id)) {
throw new BusinessException("插件未安装!");
}
installPlugins.remove(id);
}
5、到此,插件管理的接口算完成了。将PluginManager的所有代码贴出来吧
**
* 插件管理类
*
* @author zhh
* 2018/4/19 10:53
*/
@Component
public class PluginManager {
@Autowired
private ApplicationContext applicationContext;
/** 已安装的插件 */
private Map<Long, Plugin> installPlugins = new HashMap<>();
/** 已激活的插件 */
private Map<Long, Plugin> activePlugins = new HashMap<>();
/** 插件jar包存放目录 */
private static final String BASE_DIR;
/** 设置插件jar包存放目录 */
static {
BASE_DIR = System.getProperty("user.home") + "/plugin_platform/plugins";
Path baseDirPath = Paths.get(BASE_DIR);
if (Files.notExists(baseDirPath)) {
try {
Files.createDirectories(baseDirPath);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
/**
* 获取已安装插件Id列表
* @return
*/
public Set<Long> getInstallPlugins() {
return installPlugins.keySet();
}
/**
* 获取已激活插件Id列表
* @return
*/
public Set<Long> getActivePlugins() {
return activePlugins.keySet();
}
/**
* 激活插件
* @param id
*/
public void activePlugin(Long id, String[] classNameArray) throws InterruptedException, ClassNotFoundException {
if (activePlugins.containsKey(id)) {
throw new BusinessException("插件已激活!");
}
if (!installPlugins.containsKey(id)) {
throw new BusinessException("插件未安装,请先安装!");
}
Plugin plugin = installPlugins.get(id);
Advice advice = buildAdvice(plugin.getClassName());
// 获取所有的IOC Bean
Arrays.stream(applicationContext.getBeanDefinitionNames())
.map(applicationContext::getBean)
// 过滤掉当前bean和非代理bean
.filter(bean -> bean != this && bean instanceof Advised)
// 过滤掉非选中的bean
.filter(bean -> Arrays.stream(classNameArray).anyMatch(
className -> bean.getClass().getName().equalsIgnoreCase(className)
))
// 过滤掉已添加此通知的bean
.filter(bean -> {
boolean noneMatch = Arrays.stream(((Advised) bean).getAdvisors())
.map(advisor -> advisor.getAdvice().getClass().getName())
.noneMatch(adviceName -> adviceName.equalsIgnoreCase(plugin.getClassName()));
return noneMatch;})
// 添加通知
.forEach(bean ->{
((Advised)bean).addAdvice(advice);
});
activePlugins.put(id, plugin);
}
/**
* 将插件类注入IOC容器
* @param className
* @return
* @throws ClassNotFoundException
* @throws InterruptedException
*/
private Advice buildAdvice(String className) throws ClassNotFoundException, InterruptedException {
Object bean = registerBean(className);
return (Advice) bean;
}
/**
* 禁用插件
* @param id
*/
public void disablePlugin(Long id) throws InterruptedException, ClassNotFoundException {
if (!activePlugins.containsKey(id)) {
throw new BusinessException("插件未激活!");
}
Plugin advice = activePlugins.get(id);
// 获取所有的IOC Bean
String className = advice.getClassName();
Arrays.stream(applicationContext.getBeanDefinitionNames())
.map(applicationContext::getBean)
// 过滤掉当前bean
.filter(bean -> bean != this)
// 过滤到非代理bean
.filter(bean -> bean instanceof Advised)
// 禁用通知
.forEach(bean ->{
Advised advised = (Advised) bean;
Arrays.stream(advised.getAdvisors())
.map(advisor -> advisor.getAdvice())
.filter(_advice -> _advice.getClass().getName().equalsIgnoreCase(className))
.forEach(advised::removeAdvice);
});
// 将bean从IOC删除
removeBeanByClass(className);
activePlugins.remove(id);
}
/**
* 安装插件
* @param id
*/
public void installPlugin(Long id) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException {
// 是否已安装
if (installPlugins.containsKey(id)) {
throw new BusinessException(String.format("已存在指定的插件!"));
}
// 从数据库取出插件
Plugin plugin = service.getById(id);
if (plugin == null) {
throw new BusinessException("插件不存在!");
}
// jar包下载到本地
String fileName = plugin.getFileName();
byte[] fileContent = plugin.getContent();
Path jarPath = Paths.get(BASE_DIR, "/" + fileName);
// 存在就先删除,删不掉就重新命名
try {
Files.deleteIfExists(jarPath);
} catch (Exception e) {
e.printStackTrace();
Long milliSecond = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli();
jarPath = Paths.get(BASE_DIR, "/" + milliSecond + "_" + fileName);
}
InputStream in = new ByteArrayInputStream(fileContent);
Files.copy(in, jarPath);
// 加载(不连接)
URLClassLoader loader = (URLClassLoader) getClass().getClassLoader();
URL targetUrl = jarPath.toUri().toURL();
boolean isLoader = Arrays.stream(loader.getURLs()).anyMatch(url ->
url.equals(targetUrl)
);
if (!isLoader) {
Method add = URLClassLoader.class.getDeclaredMethod("addURL", new Class[] {URL.class});
// 暴力访问
add.setAccessible(true);
add.invoke(loader, targetUrl);
}
// 判断类型是否正确
String className = plugin.getClassName();
Class<?> adviceClass = loader.loadClass(className);
if (!Advice.class.isAssignableFrom(adviceClass)) {
throw new BusinessException(
String.format("配置错误,%s非%s的实现类!", className, Advice.class)
);
}
// 放进缓存
installPlugins.put(id, plugin);
}
/**
* 卸载插件
* @param id
*/
public void uninstallPlugin(Long id) {
if (!installPlugins.containsKey(id)) {
throw new BusinessException("插件未安装!");
}
installPlugins.remove(id);
}
/**
* 打开控制台
* @return
*/
public List<String> openConsole() throws IOException {
return log.getLogs();
}
/**
* 注册一个bean到IOC容器
* 使用根据类名生成的名称
* @param className 全类名
*/
public Object registerBean(String className) throws InterruptedException, ClassNotFoundException {
// 根据类名获取默认bean名
int index = className.lastIndexOf(".");
String classSimpleName = index != -1 ? className.substring(index + 1) : className;
String beanName = classSimpleName.substring(0, 1).toLowerCase()
+ classSimpleName.substring(1, classSimpleName.length());
return registerBean(className, beanName);
}
/**
* 注册一个bean到IOC容器
* 并命名为指定名称
* @param className 全类名
*/
public Object registerBean(String className, String beanName) throws ClassNotFoundException, InterruptedException {
Class<?> beanClazz = getClass().getClassLoader().loadClass(className);
// 获取bean工厂和bean声明
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory)ctx.getAutowireCapableBeanFactory();
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(beanClazz);
//动态注册bean
defaultListableBeanFactory.registerBeanDefinition(beanName, builder.getBeanDefinition());
return ctx.getBean(beanName);
}
/**
* 根据类名从IOC容器bean, 属于该类型的将全部删除
* @param className
*/
public void removeBeanByClass(String className) throws ClassNotFoundException, InterruptedException {
Class<?> beanClazz = getClass().getClassLoader().loadClass(className);
// 获取bean工厂
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory)ctx.getAutowireCapableBeanFactory();
// 获取所有bean的名称,并转为流
Arrays.stream(ctx.getBeanDefinitionNames())
// 过滤掉不属于指定类型的bean
.filter(definitionName -> {
Object bean = ctx.getBean(definitionName);
return beanClazz.isAssignableFrom(bean.getClass());
})
// 删除bean
.forEach(defaultListableBeanFactory::removeBeanDefinition);
}
/**
* 从IOC容器移除指定名称的bean
* @param beanName bean名称
*/
public boolean removeBeanByName(String beanName) throws ClassNotFoundException, InterruptedException {
// 判断指定bean是否存在
if (!ctx.containsBean(beanName)) {
return false;
}
// 获取bean工厂
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory)ctx.getAutowireCapableBeanFactory();
// 删除bean.
defaultListableBeanFactory.removeBeanDefinition(beanName);
return true;
}
/**
* 获取IOC容器中所有代理类
* @return
*/
public List<String> getAdvised() {
// 获取所有bean的类名集合
return Arrays.stream(ctx.getBeanDefinitionNames())
.map(ctx::getBean)
// 只要AOP代理的bean
.filter(AopUtils::isAopProxy)
// 获取代理对象的类名
.map(bean -> {
return bean.getClass().getName();
/*Object target = null;
try {
if(AopUtils.isJdkDynamicProxy(bean)) {
target = getJdkDynamicProxyTargetObject(bean);
} else { //cglib
target = getCglibProxyTargetObject(bean);
}
} catch (Exception e) {
e.printStackTrace();
}
return target == null ? "" : target.getClass().getName();*/
})
// 只要自定义类
.filter(className -> className.contains("cn.zhh"))
.collect(Collectors.toList());
}
/**
* 获取Cglib代理对象的目标对象
* @param proxy
* @return
* @throws Exception
*/
private Object getCglibProxyTargetObject(Object proxy) throws Exception {
Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
h.setAccessible(true);
Object dynamicAdvisedInterceptor = h.get(proxy);
Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
advised.setAccessible(true);
Object target = ((AdvisedSupport)advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();
return target;
}
/**
* 获取JDK代理对象的目标对象
* @param proxy
* @return
* @throws Exception
*/
private Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {
Field h = proxy.getClass().getSuperclass().getDeclaredField("h");
h.setAccessible(true);
AopProxy aopProxy = (AopProxy) h.get(proxy);
Field advised = aopProxy.getClass().getDeclaredField("advised");
advised.setAccessible(true);
Object target = ((AdvisedSupport)advised.get(aopProxy)).getTargetSource().getTarget();
return target;
}
@Autowired
private ApplicationContext ctx;
@Autowired
private BaseLog log;
@Autowired
private PluginService service;
四、开发插件
1、我们单独建一个Java Project,里面只有一个接口,打成jar包。用于用户开发的插件和插件平台的连接
package cn.zhh.interfaces;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
/**
* 日志接口
* @author z_hh
* @time 2018年4月20日
*/
public interface BaseLog {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* 调试级别
* @param msg
*/
void debug(String msg);
/**
* 普通级别
* @param msg
*/
void info(String msg);
/**
* 警告级别
* @param msg
*/
void warn(String msg);
/**
* 异常级别
* @param msg
*/
void error(String msg);
/**
* 获取全部日志记录
* @return
*/
List<String> getLogs();
/**
* 获取当前时间的格式化字符串
* @return
*/
default String getDateTime() {
return LocalDateTime.now().format(dtf);
}
}
2、在插件平台引入步骤1项目打成的jar包,实现该接口
public class FileLog implements BaseLog {
/**
* 日志保存集合
*/
List<String> logs = new ArrayList<>();
@Override
public void debug(String log) {
handleLog("DEBUG", log);
}
@Override
public void info(String log) {
handleLog("INFO", log);
}
@Override
public void warn(String log) {
handleLog("WARN", log);
}
@Override
public void error(String log) {
handleLog("ERROR", log);
}
private void handleLog(String logType, String log) {
log = logType + " " + log;
logs.add(log);
SocketServer.sendMessage(log, "console");
}
@Override
public List<String> getLogs() {
return logs;
}
3、新建一个Java Project项目,引入步骤1项目打成的jar包,还有两个其它jar包
(这些jar包只是用于编译,打包的时候不需要放进去)
编写环绕接口,然后将该项目打成jar包。
/**
* Around(周围) org.aopaliance.intercept.MethodInterceptor
* <p>Title: MyAroundAdvice</p>
* <p>Description: </p>
* @author z_hh
* @date 2018年4月18日
*/
public class MyAroundAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 获取方法
Method targetMethod = invocation.getMethod();
// 获取参数
Object[] args = invocation.getArguments();
// 执行目标方法并获得结果
Object result = invocation.proceed();
// 参数拼接
String argsStr = Arrays.stream(args)
.map(o -> o == null ? "" : o.toString())
.reduce((o1, o2) -> o1 + "," + "o2")
.orElse("");
// 结果转化
String resultStr = result == null ? "" : result.toString();
// 日志记录
log.info(String.format("%s 【%s:%s】参数:%s;返回值:%s",
log.getDateTime(),
this.getClass().getName(),
targetMethod.getName(),
argsStr,
resultStr));
return result;
}
@Autowired
private BaseLog log;
}
4、上传到插件管理平台
5、编写目标项目的UserServicce(必须带事务)
6、安装插件、激活插件
7、打开控制台,执行Service的方法,查看控制台输出
这里主要使用了WebSocket让客户端与服务器保持连接。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
@Configuration
public class WebSocketConfig {
/**
* 使用springboot内置tomcat进行部署的话,在编写websocket具体实现类之前,要注入ServerEndpointExporter,
* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
/**
* WebSocket服务
*
* @author z_hh
* @version 1.0
* @since 2018/4/22
*/
@ServerEndpoint("/socketServer/{userId}")
@Component
public class SocketServer {
private Session session;
private static Map<String, Session> sessionPool = new HashMap<>();
private static Map<String, String> sessionIds = new HashMap<>();
/**
* 使用该注解,在客户端初次连接时触发,这里会为客户端创建一个session,这个session并不是我们熟悉的httpsession,
* 同时我们会将客户端传来的唯一标识与该session绑定,并保存到我们定于的静态map里,便于后续的向指定客户端发送消息
* @param session
* @param userId
*/
@OnOpen
public void onOpen(Session session, @PathParam(value="userId")String userId) {
this.session = session;
sessionPool.put(userId, session);
sessionIds.put(session.getId(), userId);
}
/**
* 使用该注解,在客户端向服务端发送消息时触发,接收到来自客户端的消息
* @param message
*/
@OnMessage
public void onMessage(String message) {
System.out.println("当前发送人sessionId为" + session.getId() + ",发送内容为" + message);
}
/**
* 使用该注解,在客户端与服务端断开连接时触发,同时我们会将静态map里存储的关于此客户端的session移除掉
*/
@OnClose
public void onClose() {
sessionPool.remove(sessionIds.get(session.getId()));
sessionIds.remove(session.getId());
}
/**
* 使用该注解,在发生错误时触发
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
/**
* 根据客户端的唯一标识与要发送给客户端的消息进行对客户端的定向发送
* @param message
* @param userId
*/
public static void sendMessage(String message, String userId) {
Session session = sessionPool.get(userId);
if (session != null) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 获取在线人数
* @return
*/
public static int getOnlineCount() {
return sessionPool.size();
}
/**
* 获取所有在线用户
* @return
*/
public static String getOnlineUsers() {
return sessionIds.keySet()
.parallelStream()
.map(sessionIds::get)
.reduce((s1, s2) -> s1 + "," + s2)
.orElse("");
}
/**
* 向所有客户端发送消息
* @param message
*/
public static void sendAll(String message) {
sessionIds.keySet()
.parallelStream()
.map(sessionIds::get)
.forEach(SocketServer::sendAll);
}
}
<script type="text/javascript">
var $textarea = $(".form-control");
var ws = null;
if ('WebSocket' in window) {
ws = new WebSocket("ws://localhost:8888/socketServer/console");
}
else if ('MozWebSocket' in window) {
ws = new MozWebSocket("ws://localhost:8888/socketServer/console")
}
else {
MyAlert("该浏览器不支持websocket!");
}
ws.onmessage = function (evt) {
var oleText = $textarea.val() != "" ? $textarea.val() + "\n" : "";
$textarea.val(oleText + evt.data);
}
ws.onclose = function (evt) {
MyAlert("连接中断!");
}
ws.onopen = function (evt) {
MyAlert("连接成功!");
}
// 清空控制台
function clearConsole() {
$textarea.val("");
}
</script>