Categories
java

Ruoyi websocket 对接

若依java服务端添加websocket 包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

业务类

package com.ruoyi.project.websocket;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.security.service.TokenService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

@Component
@ServerEndpoint("/websocket")
public class WebSocketServer {
    private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class);
    
    private static final AtomicInteger onlineCount = new AtomicInteger(0);
    private static final Map<String, Session> sessionMap = new ConcurrentHashMap<>();
    private static final Map<String, Long> lastActiveTimeMap = new ConcurrentHashMap<>();
    private static final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    private static final long HEARTBEAT_TIMEOUT = 30000;
    
    private static TokenService tokenService;
    
    @Autowired
    public void setTokenService(TokenService tokenService) {
        WebSocketServer.tokenService = tokenService;
    }
    
    private String token;
    private LoginUser loginUser;
    
    static {
        scheduler.scheduleAtFixedRate(() -> {
            long currentTime = System.currentTimeMillis();
            lastActiveTimeMap.forEach((token, lastActiveTime) -> {
                if (currentTime - lastActiveTime > HEARTBEAT_TIMEOUT) {
                    Session session = sessionMap.get(token);
                    if (session != null) {
                        try {
                            session.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "心跳超时"));
                        } catch (IOException e) {
                            log.error("关闭WebSocket连接失败", e);
                        }
                    }
                }
            });
        }, 0, HEARTBEAT_TIMEOUT / 2, TimeUnit.MILLISECONDS);
    }
    
    @OnOpen
    public void onOpen(Session session) {
        log.info("有新连接加入,等待认证...");
    }
    
    @OnClose
    public void onClose() {
        if (this.token != null) {
            sessionMap.remove(this.token);
            lastActiveTimeMap.remove(this.token);
            subOnlineCount();
            log.info("连接关闭:用户ID={},当前在线人数为:{}", 
                    loginUser != null ? loginUser.getUserId() : "unknown", 
                    getOnlineCount());
        }
    }
    
    @OnMessage
    public void onMessage(String message, Session session) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            JsonNode jsonNode = mapper.readTree(message);
            
            // 处理认证消息
            if (this.token == null) {
                if (!jsonNode.has("type") || !"AUTH".equals(jsonNode.get("type").asText())) {
                    sendError(session, "请先发送认证消息");
                    return;
                }
                
                String token = jsonNode.get("token").asText();
                LoginUser user = tokenService.getLoginUser(token);
                if (user == null) {
                    sendError(session, "无效的token");
                    session.close(new CloseReason(CloseReason.CloseCodes.VIOLATED_POLICY, "认证失败"));
                    return;
                }
                
                this.token = token;
                this.loginUser = user;
                sessionMap.put(token, session);
                lastActiveTimeMap.put(token, System.currentTimeMillis());
                addOnlineCount();
                
                log.info("认证成功:用户ID={},用户名={}", user.getUserId(), user.getUsername());
                
                // 发送认证成功响应
                session.getBasicRemote().sendText(mapper.writeValueAsString(Map.of(
                    "type", "AUTH_RESPONSE",
                    "success", true,
                    "message", "认证成功",
                    "userId", user.getUserId()
                )));
                return;
            }
            
            // 更新最后活跃时间
            lastActiveTimeMap.put(this.token, System.currentTimeMillis());
            
            // 处理心跳消息
            if (jsonNode.has("type") && "HEARTBEAT".equals(jsonNode.get("type").asText())) {
                session.getBasicRemote().sendText(mapper.writeValueAsString(Map.of(
                    "type", "HEARTBEAT_ACK"
                )));
                return;
            }
            
            // 处理业务消息
            if (jsonNode.has("type") && "MESSAGE".equals(jsonNode.get("type").asText())) {
                String content = jsonNode.get("content").asText();
                log.info("收到来自用户{}的消息: {}", loginUser.getUserId(), content);
                
                // 业务处理逻辑...
                session.getBasicRemote().sendText(mapper.writeValueAsString(Map.of(
                    "type", "MESSAGE_RESPONSE",
                    "content", "服务器收到消息: " + content,
                    "timestamp", System.currentTimeMillis()
                )));
            }
            
        } catch (Exception e) {
            log.error("处理消息异常", e);
            try {
                sendError(session, "处理消息时发生错误: " + e.getMessage());
            } catch (IOException ex) {
                log.error("发送错误消息失败", ex);
            }
        }
    }
    
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("WebSocket发生错误", error);
    }
    
    private void sendError(Session session, String errorMessage) throws IOException {
        session.getBasicRemote().sendText(new ObjectMapper().writeValueAsString(Map.of(
            "type", "ERROR",
            "message", errorMessage
        )));
    }
    
    public static void sendToUser(String token, String message) {
        Session session = sessionMap.get(token);
        if (session != null && session.isOpen()) {
            try {
                session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                log.error("发送消息失败", e);
            }
        }
    }
    
    public static void sendToUserByUserId(Long userId, String message) {
        sessionMap.forEach((token, session) -> {
            try {
                LoginUser user = tokenService.getLoginUser(token);
                if (user != null && userId.equals(user.getUserId())) {
                    session.getBasicRemote().sendText(message);
                }
            } catch (Exception e) {
                log.error("通过用户ID发送消息异常", e);
            }
        });
    }
    
    public static int getOnlineCount() {
        return onlineCount.get();
    }
    
    public static void addOnlineCount() {
        onlineCount.incrementAndGet();
    }
    
    public static void subOnlineCount() {
        onlineCount.decrementAndGet();
    }
}

前端添加websocket操作类 且作为全局单例模式 任何文件都可以调用

import { getToken } from '@/utils/auth'
import { Message, MessageBox } from 'element-ui'

class WebSocketClient {
  constructor(options = {}) {
    const defaultOptions = {
      url: '',
      heartBeat: 30000,          // 心跳间隔30秒
      reconnectDelay: 5000,       // 重连延迟5秒
      maxReconnectAttempts: 5,    // 最大重连次数
      onOpen: () => {},
      onMessage: () => {},
      onClose: () => {},
      onError: () => {},
      onAuthenticated: () => {}   // 新增认证成功回调
    }

    this.options = { ...defaultOptions, ...options }
    this.ws = null
    this.reconnectAttempts = 0
    this.heartBeatTimer = null
    this.isManualClose = false
    this.isAuthenticated = false  // 认证状态
  }

  connect() {
    if (this.ws) {
      this.close()
    }

    this.ws = new WebSocket(this.options.url)

    this.ws.onopen = (event) => {
      this.reconnectAttempts = 0
      this.options.onOpen(event)
      
      // 连接建立后立即发送认证消息
      this.sendAuthMessage()
      
      this.startHeartBeat()
    }

    this.ws.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data)
        
        // 处理认证响应
        if (data.type === 'AUTH_RESPONSE') {
          if (data.success) {
            this.isAuthenticated = true
            this.options.onAuthenticated(data)
            Message.success('WebSocket认证成功')
          } else {
            Message.error(data.message || 'WebSocket认证失败')
            this.close()
          }
          return
        }
        
        // 其他消息处理
        this.options.onMessage(event)
        this.resetHeartBeat()
      } catch (e) {
        console.error('消息解析失败:', e)
      }
    }

    this.ws.onclose = (event) => {
      this.isAuthenticated = false
      this.options.onClose(event)
      this.stopHeartBeat()
      
      if (!this.isManualClose) {
        this.reconnect()
      }
    }

    this.ws.onerror = (error) => {
      this.options.onError(error)
      this.stopHeartBeat()
      
      if (!this.isManualClose) {
        this.reconnect()
      }
    }
  }

  // 发送认证消息
  sendAuthMessage() {
    const token = getToken()
    if (!token) {
      Message.error('未获取到登录Token,请重新登录')
      this.close()
      return
    }

    this.send({
      type: 'AUTH',
      token: token,
      timestamp: new Date().getTime()
    })
  }

  send(data) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data))
    } else {
      console.error('WebSocket未连接')
    }
  }

  // 其他方法保持不变...
  close() { /* ... */ }
  reconnect() { /* ... */ }
  startHeartBeat() { /* ... */ }
  stopHeartBeat() { /* ... */ }
  resetHeartBeat() { /* ... */ }
}

// 全局单例
let wsInstance = null

export function initWebSocket() {
  if (wsInstance) {
    return wsInstance
  }

  wsInstance = new WebSocketClient({
    url: process.env.VUE_APP_WS_API,
    onAuthenticated: (data) => {
      // 认证成功后的处理
      console.log('WebSocket认证成功', data)
    },
    onMessage: (event) => {
      try {
        const data = JSON.parse(event.data)
        if (data.type === 'NOTIFICATION') {
          MessageBox.alert(data.content, data.title || '系统通知', {
            confirmButtonText: '确定',
            type: data.level || 'info'
          })
        }
      } catch (e) {
        console.error('消息处理错误:', e)
      }
    },
    onError: (error) => {
      Message.error('WebSocket连接错误: ' + error.message)
    }
  })

  return wsInstance
}

export function getWebSocket() {
  return wsInstance
}

export function closeWebSocket() {
  if (wsInstance) {
    wsInstance.close()
    wsInstance = null
  }
}
Categories
java

jenkins 本地安装与调试

docker-compose

version: '3'
services:
  jenkins:
    image: jenkins/jenkins
    container_name: jenkins
    restart: unless-stopped  #指定容器退出后的重启策略为始终重启,但是不考虑在Docker守护进程启动时就已经停止了的容器
    volumes:
      - "/usr/bin/docker:/usr/bin/docker"
      - "/var/run/docker.sock:/var/run/docker.sock"
      - "/usr/lib64/libltdl.so.7:/usr/lib/x86_64-linux-gnu/libltdl.so.7"
      - "./jenkins/jenkins_home:/var/jenkins_home"
      - "./jenkins/jenkins_config:/var/jenkins_config"
    environment:
      http_proxy: 'http://192.168.0.12:33333'
      https_proxy: 'http://192.168.0.12:33333'
      TZ: Asia/Shanghai
      LANG: en_US.UTF-8
      JAVA_OPTS: '-Xmx2048M -Xms2048M -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:MaxNewSize=128m -Djava.util.logging.config.file=/var/jenkins_home/log.properties -Duser.timezone=Asia/Shanghai'
    user: root
    ports:
      - "10000:8080"
      

由于默认插件地址是国外的 没有插件jenkins可就没啥用了

所以安装完成后在插件市场 plugin manager设置 插件更新地址

https://eastamerica.cloudflare.jenkins.io/current/update-center.json

Categories
java

jwt token 和 sa-token

JWTSa-Token在功能、使用场景和优缺点等方面存在显著差异。

功能和适用场景

  • JWT‌:JWT是一种无状态令牌协议标准(RFC 7519),主要用于在网络间安全传递声明(Claims)。其核心特点包括无状态性、自包含结构(Header.Payload.Signature三段式编码)和跨域支持,特别适合分布式系统和跨服务认证‌1。JWT通常用于身份验证和信息交换,特别是在无状态的客户端-服务器通信中。由于其简洁和标准化的特点,JWT可以与各种语言和技术栈配合使用,提高系统的灵活性和可扩展性‌2。
  • Sa-Token‌:这是一款轻量级权限认证框架,专注于解决系统的登录认证、权限管理、会话管理等安全问题。其核心特点包括有状态会话管理(基于服务端存储会话,如Redis)、开箱即用的API(提供注解式鉴权、踢人下线、单点登录等功能)和多存储适配(支持Redis、Memcached、数据库等会话存储方式)‌12。Sa-Token提供了丰富的权限相关注解,支持多种条件组合,如AND、OR逻辑关系,以及复杂的表达式,适合需要快速开发和易于维护的项目‌2。

优缺点对比

  • JWT‌:
    • 优点‌:无状态性减少了服务器的存储压力,提高了系统的可扩展性和跨域支持;自包含结构使得令牌本身包含所有必要信息,无需服务端存储;简洁和标准化使其与各种语言和技术栈兼容‌12。
    • 缺点‌:需要自行实现令牌黑名单机制以实现踢人功能;由于信息自包含,令牌体积较大,可能会影响传输效率‌13。
  • Sa-Token‌:
    • 优点‌:提供丰富的权限相关注解和功能,支持多种存储方式,适合快速开发和易于维护的项目;开箱即用的体验简化了权限认证的集成和使用过程‌12。
    • 缺点‌:有状态会话管理增加了服务器的存储压力;虽然功能丰富,但在高度定制化安全策略方面可能不如Spring Security灵活‌2。
Categories
java

若依 本地docker部署

Dockerfile :

FROM  docker.1ms.run/openjdk:8

MAINTAINER Robin Luo

RUN mkdir -p /ruoyi/server/logs \
    /ruoyi/server/temp \
    /ruoyi/skywalking/agent

WORKDIR /ruoyi/server

ENV SERVER_PORT=8080

EXPOSE ${SERVER_PORT}

ADD ./ruoyi-admin.jar ./app.jar

ENTRYPOINT ["java", \
            "-Djava.security.egd=file:/dev/./urandom", \
            "-Dserver.port=${SERVER_PORT}", \
            # 应用名称 如果想区分集群节点监控 改成不同的名称即可
#            "-Dskywalking.agent.service_name=ruoyi-server", \
#            "-javaagent:/ruoyi/skywalking/agent/skywalking-agent.jar", \
            "-jar", "app.jar"]

docker-compose.yaml

version: '3.8'
services:
  ruoyi-his-server:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: ruoyi-his-server
    ports:
      - "8009:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=druid,dev
    restart: always

再该目录下 执行 docker-compose up -d 启动服务

执行 dock-compose down 停止服务

Categories
java

DWR

DWR 全称 direct web remote

可将java的一个对象当成js一个对象执行方法返回结果

可以理解为js域通向java服务端的一种RPC(远程方法调用)

技术执行流 为 js-> js 对象执行方法-> post 异步请求->DWRServlet->java 对象

中间post 异步请求与 DWRServlet 均被封装屏蔽

用户只需要调用DWRServlet 动态生成的js脚本里面生成的js对象的方法 就可以发起通讯

Categories
java

gradle compile tree

gradle import local jar as dependency

dependencies{
// 依赖某个jar文件
implementation files(‘lib/xxx.jar’)


// 依赖libs目录下所有以.jar结尾的文件
implementation fileTree(dir: ‘lib’, include: [‘.jar’])

// 依赖libs目录下除了xxx.jar以外的所有以.jar结尾的文件 implementation fileTree(dir: ‘lib’, exclude: [‘xxx.jar’], include: [‘.jar’])

}

Categories
java

aliyun maven

仓库名称阿里云仓库地址阿里云仓库地址(老版)源地址
centralhttps://maven.aliyun.com/repository/centralhttps://maven.aliyun.com/nexus/content/repositories/centralhttps://repo1.maven.org/maven2/
jcenterhttps://maven.aliyun.com/repository/publichttps://maven.aliyun.com/nexus/content/repositories/jcenterhttp://jcenter.bintray.com/
publichttps://maven.aliyun.com/repository/publichttps://maven.aliyun.com/nexus/content/groups/publiccentral仓和jcenter仓的聚合仓
googlehttps://maven.aliyun.com/repository/googlehttps://maven.aliyun.com/nexus/content/repositories/googlehttps://maven.google.com/
gradle-pluginhttps://maven.aliyun.com/repository/gradle-pluginhttps://maven.aliyun.com/nexus/content/repositories/gradle-pluginhttps://plugins.gradle.org/m2/
springhttps://maven.aliyun.com/repository/springhttps://maven.aliyun.com/nexus/content/repositories/springhttp://repo.spring.io/libs-milestone/
spring-pluginhttps://maven.aliyun.com/repository/spring-pluginhttps://maven.aliyun.com/nexus/content/repositories/spring-pluginhttp://repo.spring.io/plugins-release/
grails-corehttps://maven.aliyun.com/repository/grails-corehttps://maven.aliyun.com/nexus/content/repositories/grails-corehttps://repo.grails.org/grails/core
apache snapshotshttps://maven.aliyun.com/repository/apache-snapshotshttps://maven.aliyun.com/nexus/content/repositories/apache-snapshotshttps://repository.apache.org/snapshots/

maven 版本

<repository>
  <id>spring</id>
  <url>https://maven.aliyun.com/repository/spring</url>
  <releases>
    <enabled>true</enabled>
  </releases>
  <snapshots>
    <enabled>true</enabled>
  </snapshots>
</repository>

gradle 版本

allProjects {
  repositories {
    maven {
      url 'https://maven.aliyun.com/repository/public/'
    }
    maven {
      url 'https://maven.aliyun.com/repository/spring/'
    }
    mavenLocal()
    mavenCentral()
  }
}
Categories
java Uncategorized

activiti todo

流程定义模块-> 设计-> 部署 删除

流程定义历史模块

流程实例模块

任务实例模块

Categories
java

spring activiti starter

<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-spring-boot-starter</artifactId>
    <version>7.1.0.M6</version>
</dependency>

注入activiti 组件


初始化activiti 表

以下单元测试用例 用来初始化 activiti 表
@SpringBootTest
class ActivitiApplicationTests {

    @Test
    void contextLoads() {
        ProcessEngineConfiguration processEngineConfiguration = ProcessEngineConfiguration.createProcessEngineConfigurationFromResource("activiti.cfg.xml");
        processEngineConfiguration.buildProcessEngine();
    }

}

activiti.cfg.xml 是在类目录下的 一个spring beans 配置文件

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans   http://www.springframework.org/schema/beans/spring-beans.xsd">


    <bean id="dataSource" class="com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceWrapper" >
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://mysql.robinluo.top/activiti?characterEncoding=UTF-8&amp;nullCatalogMeansCurrent=true&amp;serverTimezone=GMT&amp;useSSL=false" />
        <property name="username" value="admin" />
        <property name="password" value="RobinLuo@2021" />
    </bean>


    <bean id="processEngineConfiguration" class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
        <!-- ... -->
        <property name="databaseSchemaUpdate" value="true" />
        <property name="dataSource" ref="dataSource" />
    </bean>

</beans>
Categories
java

Druid Spring Starter

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.8</version>
</dependency>


自动注入druid datasource