diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml index 830ba5f..c5d7008 100644 --- a/ruoyi-common/pom.xml +++ b/ruoyi-common/pom.xml @@ -188,7 +188,10 @@ spring-boot-starter-mail 3.1.4 - + + org.springframework.boot + spring-boot-starter-websocket + javax.activation activation diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/VoidFunction.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/VoidFunction.java new file mode 100644 index 0000000..3022ec0 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/VoidFunction.java @@ -0,0 +1,13 @@ +package com.ruoyi.common.annotation; +/** + * 一个参数、没有返回 + * @author wangqiong + */ +@FunctionalInterface +public interface VoidFunction { + /** + * 有一个参数 + * @param param + */ + void apply(T param); +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java index 5b5ab5a..7fdb47f 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/Constants.java @@ -129,6 +129,7 @@ public class Constants */ public static final String LOOKUP_LDAPS = "ldaps:"; public static final String DEFAULT_PASSWORD = "123456"; + public static final String SPLIT_COMMA =","; /** * 自动识别json对象白名单配置(仅允许解析的包名,范围越小越安全) diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/thread/MultipleThreadListParam.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/thread/MultipleThreadListParam.java new file mode 100644 index 0000000..71c0882 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/thread/MultipleThreadListParam.java @@ -0,0 +1,22 @@ +package com.ruoyi.common.utils.thread; + +import lombok.Data; + +import java.util.List; +import java.util.function.Function; + +/** + * @author wangqiong + * @date 2023/11/25 9:25 + **/ +@Data +public class MultipleThreadListParam { + /**需要执行的方法*/ + private Function,R> function; + /**参数ID*/ + private List list; + public MultipleThreadListParam(Function,R> function, List list){ + this.function=function; + this.list=list; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/thread/MultipleThreadStringParam.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/thread/MultipleThreadStringParam.java new file mode 100644 index 0000000..32fa680 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/thread/MultipleThreadStringParam.java @@ -0,0 +1,21 @@ +package com.ruoyi.common.utils.thread; + +import lombok.Data; + +import java.util.function.Function; + +/** + * @author wangqiong + * @date 2023/11/25 9:25 + **/ +@Data +public class MultipleThreadStringParam { + /**需要执行的方法*/ + private Function function; + /**参数ID*/ + private String ids; + public MultipleThreadStringParam(Function function, String ids){ + this.function=function; + this.ids=ids; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/thread/MultipleThreadWorkUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/thread/MultipleThreadWorkUtil.java new file mode 100644 index 0000000..81b9023 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/thread/MultipleThreadWorkUtil.java @@ -0,0 +1,547 @@ +package com.ruoyi.common.utils.thread; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import com.ruoyi.common.annotation.VoidFunction; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.exception.ServiceException; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.concurrent.*; +import java.util.function.Function; + +/** + * @description 多线程操作数据库,一个线程异常全部回滚 + * @Author wangqiong + * @Date 2023/11/25 14:02 + * @Version V1.0 + **/ +@Slf4j +public class MultipleThreadWorkUtil { + private static int SIMPLE_TIME_COUNT=1000; + + public static List exec(Function,R> execFun, List list){ + List returnList=new ArrayList<>(); + if(CollectionUtil.isEmpty(list)){ + return returnList; + } + if(list.size()0){ + times++; + } + CountDownLatch mainLatch=new CountDownLatch(1); + //监控子线程 + CountDownLatch threadLatch=new CountDownLatch(times); + //根据子线程执行结果判断是否需要回滚 + BlockingDeque resultList=new LinkedBlockingDeque<>(); + //必须使用对象,如果使用变量会造成线程之间不能共享变量值 + RollBack rollBack=new RollBack(false); + ExecutorService executorService=Executors.newFixedThreadPool(times); + List> futureList=new ArrayList<>(); + for (int i = 0; i future=executorService.submit(new ExecThread(mainLatch,threadLatch,rollBack,resultList,list.subList(i*SIMPLE_TIME_COUNT,i*SIMPLE_TIME_COUNT+SIMPLE_TIME_COUNT),execFun)); + futureList.add(future); + }else{ + Future future=executorService.submit(new ExecThread(mainLatch,threadLatch,rollBack,resultList,list.subList(i*SIMPLE_TIME_COUNT,list.size()),execFun)); + futureList.add(future); + } + } + setResult(executorService,returnList,futureList,resultList, + mainLatch,threadLatch,rollBack,times); + return returnList; + } + + public static void exec(VoidFunction> execFun, Set set){ + if(set.size()0){ + times++; + } + CountDownLatch mainLatch=new CountDownLatch(1); + //监控子线程 + CountDownLatch threadLatch=new CountDownLatch(times); + //根据子线程执行结果判断是否需要回滚 + BlockingDeque resultList=new LinkedBlockingDeque<>(); + //必须使用对象,如果使用变量会造成线程之间不能共享变量值 + RollBack rollBack=new RollBack(false); + ExecutorService executorService=Executors.newFixedThreadPool(times); + List list=new ArrayList<>(set); + List futureList=new ArrayList<>(); + for (int i = 0; i backUpResult=new ArrayList<>(); + try{ + // + boolean await=threadLatch.await(times*3,TimeUnit.SECONDS); + if(!await){ + rollBack.setRollBack(true); + }else{ + //查看执行情况,如果有存在需要回滚的线程,则全部回滚 + for (int i = 0; i List execFun(MultipleThreadStringParam...params){ + List returnList=new ArrayList<>(); + if(ArrayUtil.isEmpty(params)){ + return returnList; + } + List threadCountList=new ArrayList<>(); + for (MultipleThreadStringParam param : params) { + List idList= Arrays.asList(param.getIds().split(Constants.SPLIT_COMMA)); + if(idList.size()0){ + threadCountList.add(idList.size()/SIMPLE_TIME_COUNT+1); + }else{ + threadCountList.add(idList.size()/SIMPLE_TIME_COUNT); + } + } + } + int times=0; + for (Integer count : threadCountList) { + times+=count; + } + CountDownLatch mainLatch=new CountDownLatch(1); + //监控子线程 + CountDownLatch threadLatch=new CountDownLatch(times); + //根据子线程执行结果判断是否需要回滚 + BlockingDeque resultList=new LinkedBlockingDeque<>(); + //必须使用对象,如果使用变量会造成线程之间不能共享变量值 + RollBack rollBack=new RollBack(false); + ExecutorService executorService=Executors.newFixedThreadPool(times); + List> futureList=new ArrayList<>(); + for (int i = 0; i < params.length; i++) { + MultipleThreadStringParam param=params[i]; + List idList= Arrays.asList(param.getIds().split(Constants.SPLIT_COMMA)); + for (int j = 0; j future=executorService.submit(new ExecByIdsStringThread<>(mainLatch,threadLatch,rollBack,resultList,StrUtil.join(Constants.SPLIT_COMMA,idList.subList(j*SIMPLE_TIME_COUNT,j*SIMPLE_TIME_COUNT+SIMPLE_TIME_COUNT)),param.getFunction())); + futureList.add(future); + }else{ + Future future=executorService.submit(new ExecByIdsStringThread(mainLatch,threadLatch,rollBack,resultList,StrUtil.join(Constants.SPLIT_COMMA,idList.subList(j*SIMPLE_TIME_COUNT,idList.size())),param.getFunction())); + futureList.add(future); + } + } + } + setResult(executorService,returnList,futureList,resultList, + mainLatch,threadLatch,rollBack,times); + return returnList; + } + + public static List execByIds(Function execFun,String ids){ + List returnList=new ArrayList<>(); + if(StrUtil.isEmpty(ids)){ + return returnList; + } + List idList= Arrays.asList(ids.split(Constants.SPLIT_COMMA)); + if(idList.size()0){ + times++; + } + CountDownLatch mainLatch=new CountDownLatch(1); + //监控子线程 + CountDownLatch threadLatch=new CountDownLatch(times); + //根据子线程执行结果判断是否需要回滚 + BlockingDeque resultList=new LinkedBlockingDeque<>(); + //必须使用对象,如果使用变量会造成线程之间不能共享变量值 + RollBack rollBack=new RollBack(false); + ExecutorService executorService=Executors.newFixedThreadPool(times); + List> futureList=new ArrayList<>(); + for (int i = 0; i future=executorService.submit(new ExecByIdsStringThread<>(mainLatch,threadLatch,rollBack,resultList,StrUtil.join(Constants.SPLIT_COMMA,idList.subList(i*SIMPLE_TIME_COUNT,i*SIMPLE_TIME_COUNT+SIMPLE_TIME_COUNT)),execFun)); + futureList.add(future); + }else{ + Future future=executorService.submit(new ExecByIdsStringThread(mainLatch,threadLatch,rollBack,resultList,StrUtil.join(Constants.SPLIT_COMMA,idList.subList(i*SIMPLE_TIME_COUNT,idList.size())),execFun)); + futureList.add(future); + } + } + setResult(executorService,returnList,futureList,resultList, + mainLatch,threadLatch,rollBack,times); + return returnList; + } + + public static List execByIds(Function,List> execFun,List idList){ + List returnList=new ArrayList<>(); + if(CollectionUtil.isEmpty(idList)){ + return returnList; + } + if(idList.size()0){ + times++; + } + CountDownLatch mainLatch=new CountDownLatch(1); + //监控子线程 + CountDownLatch threadLatch=new CountDownLatch(times); + //根据子线程执行结果判断是否需要回滚 + BlockingDeque resultList=new LinkedBlockingDeque<>(); + //必须使用对象,如果使用变量会造成线程之间不能共享变量值 + RollBack rollBack=new RollBack(false); + ExecutorService executorService=Executors.newFixedThreadPool(times); + List>> futureList=new ArrayList<>(); + for (int i = 0; i > future=executorService.submit(new ExecByIdsStringListThread<>(mainLatch,threadLatch,rollBack,resultList,idList.subList(i*SIMPLE_TIME_COUNT,i*SIMPLE_TIME_COUNT+SIMPLE_TIME_COUNT),execFun)); + futureList.add(future); + }else{ + Future> future=executorService.submit(new ExecByIdsStringListThread(mainLatch,threadLatch,rollBack,resultList,idList.subList(i*SIMPLE_TIME_COUNT,idList.size()),execFun)); + futureList.add(future); + } + } + /**存放子线程返回结果*/ + List backUpResult=new ArrayList<>(); + try{ + // + boolean await=threadLatch.await(times*3,TimeUnit.SECONDS); + if(!await){ + rollBack.setRollBack(true); + }else{ + //查看执行情况,如果有存在需要回滚的线程,则全部回滚 + for (int i = 0; i > future : futureList) { + try { + returnList.addAll(future.get()); + } catch (Exception e) { + throw new ServiceException("多线程执行异常"); + } + } + return returnList; + } + + private static void setResult(ExecutorService executorService,List returnList,List> futureList,BlockingDeque resultList + ,CountDownLatch mainLatch,CountDownLatch threadLatch,RollBack rollBack,int times){ + /**存放子线程返回结果*/ + List backUpResult=new ArrayList<>(); + try{ + // + boolean await=threadLatch.await(times*3,TimeUnit.SECONDS); + if(!await){ + rollBack.setRollBack(true); + }else{ + //查看执行情况,如果有存在需要回滚的线程,则全部回滚 + for (int i = 0; i future : futureList) { + try { + returnList.add(future.get()); + } catch (Exception e) { + throw new ServiceException("多线程执行异常"); + } + } + } + + static class QueryThread implements Callable>{ + private String ids; + private Function> execFun; + public QueryThread(String ids,Function> execFun){ + this.ids=ids; + this.execFun=execFun; + } + @Override + public List call(){ + return execFun.apply(ids); + } + } + + static class ExecThread implements Callable{ + /**主线程监控*/ + private CountDownLatch mainLatch; + /**子线程监控*/ + private CountDownLatch threadLatch; + /**是否回滚*/ + private RollBack rollBack; + private BlockingDeque resultList; + private List list; + private Function,R> execFun; + public ExecThread(CountDownLatch mainLatch,CountDownLatch threadLatch,RollBack rollBack,BlockingDeque resultList,List list,Function,R> execFun){ + this.mainLatch=mainLatch; + this.threadLatch=threadLatch; + this.rollBack=rollBack; + this.resultList=resultList; + this.list=list; + this.execFun=execFun; + } + @Override + @Transactional(rollbackFor = Exception.class) + public R call(){ + // 是否回滚 + Boolean result=false; + R r=null; + try{ + // 对数据库进行操作 + r=execFun.apply(list); + }catch (Exception e){ + e.printStackTrace(); + result=true; + } + resultList.add(result); + // 子线程-1,切换到主线程执行 + threadLatch.countDown(); + try{ + // 等待主线程执行 + mainLatch.await(); + }catch (InterruptedException e){ + throw new ServiceException("多线程执行异常"); + } + if(rollBack.getRollBack()){ + throw new ServiceException("多线程执行异常"); + } + return r; + } + } + + static class VoidExecThread implements Runnable{ + /**主线程监控*/ + private CountDownLatch mainLatch; + /**子线程监控*/ + private CountDownLatch threadLatch; + /**是否回滚*/ + private RollBack rollBack; + private BlockingDeque resultList; + private List list; + private VoidFunction> execFun; + public VoidExecThread(CountDownLatch mainLatch,CountDownLatch threadLatch,RollBack rollBack,BlockingDeque resultList,List list,VoidFunction> execFun){ + this.mainLatch=mainLatch; + this.threadLatch=threadLatch; + this.rollBack=rollBack; + this.resultList=resultList; + this.list=list; + this.execFun=execFun; + } + @Override + public void run() { + Boolean result=false; + try{ + execFun.apply(new HashSet<>(list)); + }catch (Exception e){ + e.printStackTrace(); + result=true; + } + resultList.add(result); + threadLatch.countDown(); + try{ + mainLatch.await(); + }catch (InterruptedException e){ + throw new ServiceException("多线程执行异常"); + } + if(rollBack.getRollBack()){ + throw new ServiceException("多线程执行异常"); + } + } + } + + + static class ExecByIdsStringThread implements Callable{ + /**主线程监控*/ + private CountDownLatch mainLatch; + /**子线程监控*/ + private CountDownLatch threadLatch; + /**是否回滚*/ + private RollBack rollBack; + private BlockingDeque resultList; + private String ids; + private Function execFun; + public ExecByIdsStringThread(CountDownLatch mainLatch,CountDownLatch threadLatch,RollBack rollBack,BlockingDeque resultList,String ids,Function execFun){ + this.mainLatch=mainLatch; + this.threadLatch=threadLatch; + this.rollBack=rollBack; + this.resultList=resultList; + this.ids=ids; + this.execFun=execFun; + } + @Override + public R call(){ + Boolean result=false; + R r=null; + try{ + r=execFun.apply(ids); + }catch (Exception e){ + e.printStackTrace(); + result=true; + } + resultList.add(result); + threadLatch.countDown(); + try{ + mainLatch.await(); + }catch (InterruptedException e){ + throw new ServiceException("多线程执行异常"); + } + if(rollBack.getRollBack()){ + throw new ServiceException("多线程执行异常"); + } + return r; + } + } + + + + + static class ExecByIdsStringListThread implements Callable{ + /**主线程监控*/ + private CountDownLatch mainLatch; + /**子线程监控*/ + private CountDownLatch threadLatch; + /**是否回滚*/ + private RollBack rollBack; + private BlockingDeque resultList; + private List ids; + private Function,R> execFun; + public ExecByIdsStringListThread(CountDownLatch mainLatch,CountDownLatch threadLatch,RollBack rollBack,BlockingDeque resultList,List ids,Function,R> execFun){ + this.mainLatch=mainLatch; + this.threadLatch=threadLatch; + this.rollBack=rollBack; + this.resultList=resultList; + this.ids=ids; + this.execFun=execFun; + } + @Override + public R call(){ + Boolean result=false; + R r=null; + try{ + r=execFun.apply(ids); + }catch (Exception e){ + e.printStackTrace(); + result=true; + } + resultList.add(result); + threadLatch.countDown(); + try{ + mainLatch.await(); + }catch (InterruptedException e){ + throw new ServiceException("多线程执行异常"); + } + if(rollBack.getRollBack()){ + throw new ServiceException("多线程执行异常"); + } + return r; + } + } + + @Data + static class RollBack{ + private Boolean rollBack; + public RollBack(Boolean rollBack){ + this.rollBack=rollBack; + } + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/websocket/MyWebSocketHandler.java b/ruoyi-common/src/main/java/com/ruoyi/common/websocket/MyWebSocketHandler.java new file mode 100644 index 0000000..4da9fcb --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/websocket/MyWebSocketHandler.java @@ -0,0 +1,140 @@ +package com.ruoyi.common.websocket; + + +import com.ruoyi.common.exception.ServiceException; +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.HashMap; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * @author wangqiong + * @description + * @date 2023-11-25 15:16 + */ +@Component +@Slf4j +@ServerEndpoint("/websocket/{userName}") +public class MyWebSocketHandler { + // 接口路径 ws://127.0.0.1:9000/websocket; + private Session session; + + //concurrent包的线程安全Set,用来存放每个客户端对应的WebSocket对象。 + private static CopyOnWriteArraySet sessions = new CopyOnWriteArraySet<>(); + // 用来存在线连接数 + private static Map sessionPool = new HashMap<>(); + + /** + * 链接成功调用的方法 + */ + @OnOpen + public void onOpen(Session session, @PathParam(value = "userName") String userName) { + try { + sessions.add(session); + sessionPool.put(userName, session); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 链接关闭调用的方法 + */ + @OnClose + public void onClose(Session session,@PathParam(value = "userName") String userName) { + try { + if(session!=null && session.isOpen()){ + session.close(); + sessions.remove(session); + sessionPool.remove(userName); + } + + log.info("【websocket消息】连接断开,总数为:" + sessions.size()); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 收到客户端消息后调用的方法 + * + * @param message + * @param + */ + @OnMessage + public void onMessage(@PathParam(value = "userName") String userName, String message) { + + System.out.println("【websocket消息】收到客户端消息:" + message); + // 将消息广播给其它用户 + for (Session session : sessions) { + if(session!=null && session.isOpen()){ + try { + session.getBasicRemote().sendText(message); + }catch (Exception e){ + throw new ServiceException(e.getMessage()); + } + }else { + try { + session.close(); + } catch (IOException e) { + throw new ServiceException(e.getMessage()); + } + sessions.remove(session); + sessionPool.remove(userName); + } + } + + } + + /** + * 发送错误时的处理 + * + * @param session + * @param error + */ + @OnError + public void onError(Session session, Throwable error) { + + throw new ServiceException(error.getMessage()); + } + + + /** + * 推消息给前端 + * + * @param userId + * @param message + * @return + */ + public static Runnable sendOneMessage(String userId, String message) { + Session session = sessionPool.get(userId); + if (session != null && session.isOpen()) { + try { + log.info("【推给前端消息】 :" + message); + + //高并发下,防止session占用期间,被其他线程调用 + synchronized (session) { + session.getBasicRemote().sendText(message); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + return null; + } + + + + + +} + diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/websocket/WebSocketConfig.java b/ruoyi-common/src/main/java/com/ruoyi/common/websocket/WebSocketConfig.java new file mode 100644 index 0000000..1084cbd --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/websocket/WebSocketConfig.java @@ -0,0 +1,26 @@ +package com.ruoyi.common.websocket;//package com.ruoyi.common.websocket; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +/** + * @author wangqiong + * @description + * @date 2023-11-25 15:19 + */ + +@Configuration +public class WebSocketConfig { + + /** + * 注入ServerEndpointExporter, + * 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint + */ + + @Bean + public ServerEndpointExporter serverEndpointExporter() { + return new ServerEndpointExporter(); + } + +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java index 7078344..aa522ae 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java @@ -111,7 +111,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter // 过滤请求 .authorizeRequests() // 对于登录login 注册register 验证码captchaImage 允许匿名访问 - .antMatchers("/login", "/register", "/captchaImage","/uploadPath/**").permitAll() + .antMatchers("/login", "/register", "/captchaImage","/uploadPath/**","/websocket/**").permitAll() // 静态资源,可匿名访问 .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()