netty之Netty集群部署实现跨服务端通信的落地方案

news2024/11/24 3:47:56

前言


在一些小型用户体量的socket服务内,仅部署单台机器就可以满足业务需求。但当遇到一些中大型用户体量的服务时,就需要考虑讲Netty按照集群方式部署,以更好的满足业务诉求。但Netty部署集群后都会遇到跨服务端怎么通信,也就是有集群服务X和Y,用户A链接服务X,用户B链接服务Y,那么他们都不在一个服务内怎么通信?本章节将介绍一种实现方式案例,以满足跨服务之间的用户通信。
涉及到的技术点
1:跨服务之间案例采用redis的发布和订阅进行传递消息,如果你是大型服务可以使用zookeeper
2:用户A在发送消息给用户B时候,需要传递B的channeId,以用于服务端进行查找channeId所属是否自己的服务内
3:单台机器也可以启动多个Netty服务,程序内会自动寻找可用端口
代码目录结构
在这里插入图片描述
domain/MsgAgreement.java | 定义信息传输协议,这个看似简单但非常重要,每一个通信的根本就是定义传输协议信息。

package com.lm.demo.netty.domain;


public class MsgAgreement {

    private String toChannelId; //发送给某人,某人channelId
    private String content;     //消息内容

    public MsgAgreement() {
    }

    public MsgAgreement(String toChannelId, String content) {
        this.toChannelId = toChannelId;
        this.content = content;
    }

    public String getToChannelId() {
        return toChannelId;
    }

    public void setToChannelId(String toChannelId) {
        this.toChannelId = toChannelId;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
    
}

PublisherConfig.java | redis消息发布者,集成与SpringBoot的配置方式

package com.lm.demo.netty.redis.config;

import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;


@Configuration
public class PublisherConfig {

    @Bean
    public RedisTemplate<String, Object> redisMessageTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setDefaultSerializer(new FastJsonRedisSerializer<>(Object.class));
        return template;
    }

}

redis/config/ReceiverConfig.java | redis消息的订阅者,集成与SpringBoot的配置方式。可以订阅多个主题

package com.lm.demo.netty.redis.config;

import com.lm.demo.netty.redis.MsgAgreementReceiver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;


@Configuration
public class ReceiverConfig {

    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, MessageListenerAdapter msgAgreementListenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(msgAgreementListenerAdapter, new PatternTopic("itstack-demo-netty-push-msgAgreement"));
        return container;
    }

    @Bean
    public MessageListenerAdapter msgAgreementListenerAdapter(MsgAgreementReceiver receiver) {
        return new MessageListenerAdapter(receiver, "receiveMessage");
    }

}

MsgAgreementReceiver.java | 实现抽象类用于接收订阅到的消息,接收消息后进行业务处理

package com.lm.demo.netty.redis;

import com.alibaba.fastjson.JSON;
import com.lm.demo.netty.domain.MsgAgreement;
import com.lm.demo.netty.util.CacheUtil;
import io.netty.channel.Channel;
import com.lm.demo.netty.util.MsgUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class MsgAgreementReceiver extends AbstractReceiver {

    private Logger logger = LoggerFactory.getLogger(MsgAgreementReceiver.class);

    @Override
    public void receiveMessage(Object message) {
        logger.info("接收到PUSH消息:{}", message);
        MsgAgreement msgAgreement = JSON.parseObject(message.toString(), MsgAgreement.class);
        String toChannelId = msgAgreement.getToChannelId();
        Channel channel = CacheUtil.cacheChannel.get(toChannelId);
        if (null == channel) return;
        channel.writeAndFlush(MsgUtil.obj2Json(msgAgreement));
    }

}

/RedisUtil.java | redis操作工具类,帮助存储数据。以下是将链接到服务的用户信息存放到redis方便可以在每个服务端都能看到这份用户链接数据

package com.lm.demo.netty.redis;

import com.alibaba.fastjson.JSON;
import com.lm.demo.netty.domain.UserChannelInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;


@Service("redisUtil")
public class RedisUtil {

    @Autowired
    private StringRedisTemplate redisTemplate;

    public void pushObj(UserChannelInfo userChannelInfo) {
        redisTemplate.opsForHash().put("demo-netty-2-09-user", userChannelInfo.getChannelId(), JSON.toJSONString(userChannelInfo));
    }

    public List<UserChannelInfo> popList() {
        List<Object> values = redisTemplate.opsForHash().values("demo-netty-2-09-user");
        if (null == values) return new ArrayList<>();
        List<UserChannelInfo> userChannelInfoList = new ArrayList<>();
        for (Object strJson : values) {
            userChannelInfoList.add(JSON.parseObject(strJson.toString(), UserChannelInfo.class));
        }
        return userChannelInfoList;
    }

    public void remove(String channelId) {
        redisTemplate.opsForHash().delete("demo-netty-2-09-user",channelId);
    }

    public void clear(){
        redisTemplate.delete("demo-netty-2-09-user");
    }

}

MyServerHandler.java | 处理接收到的信息,尤其在channelRead中,将接受者不是本服务端的用户,进行全局push

package com.lm.demo.netty.server;

import com.lm.demo.netty.domain.MsgAgreement;
import com.lm.demo.netty.domain.UserChannelInfo;
import com.lm.demo.netty.service.ExtServerService;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.socket.SocketChannel;
import com.lm.demo.netty.util.CacheUtil;
import com.lm.demo.netty.util.MsgUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.SimpleDateFormat;
import java.util.Date;


public class MyServerHandler extends ChannelInboundHandlerAdapter {

    private Logger logger = LoggerFactory.getLogger(MyServerHandler.class);

    private ExtServerService extServerService;

    public MyServerHandler(ExtServerService extServerService) {
        this.extServerService = extServerService;
    }

    /**
     * 当客户端主动链接服务端的链接后,这个通道就是活跃的了。也就是客户端与服务端建立了通信通道并且可以传输数据
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        SocketChannel channel = (SocketChannel) ctx.channel();
        System.out.println("链接报告开始");
        System.out.println("链接报告信息:有一客户端链接到本服务端。channelId:" + channel.id());
        System.out.println("链接报告IP:" + channel.localAddress().getHostString());
        System.out.println("链接报告Port:" + channel.localAddress().getPort());
        System.out.println("链接报告完毕");

        //保存用户信息
        UserChannelInfo userChannelInfo = new UserChannelInfo(channel.localAddress().getHostString(), channel.localAddress().getPort(), channel.id().toString(), new Date());
        extServerService.getRedisUtil().pushObj(userChannelInfo);
        CacheUtil.cacheChannel.put(channel.id().toString(), channel);
        //通知客户端链接建立成功
        String str = "通知客户端链接建立成功" + " " + new Date() + " " + channel.localAddress().getHostString() + "\r\n";
        ctx.writeAndFlush(MsgUtil.buildMsg(channel.id().toString(), str));

    }

    /**
     * 当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端的关闭了通信通道并且不可以传输数据
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("客户端断开链接" + ctx.channel().localAddress().toString());
        extServerService.getRedisUtil().remove(ctx.channel().id().toString());
        CacheUtil.cacheChannel.remove(ctx.channel().id().toString(), ctx.channel());
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object objMsgJsonStr) throws Exception {
        //接收msg消息{与上一章节相比,此处已经不需要自己进行解码}
        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 接收到消息内容:" + objMsgJsonStr);

        MsgAgreement msgAgreement = MsgUtil.json2Obj(objMsgJsonStr.toString());

        String toChannelId = msgAgreement.getToChannelId();
        //判断接收消息用户是否在本服务端
        Channel channel = CacheUtil.cacheChannel.get(toChannelId);
        if (null != channel) {
            channel.writeAndFlush(MsgUtil.obj2Json(msgAgreement));
            return;
        }
        //如果为NULL则接收消息的用户不在本服务端,需要push消息给全局
        logger.info("接收消息的用户不在本服务端,PUSH!");
        extServerService.push(msgAgreement);
    }

    /**
     * 抓住异常,当发生异常的时候,可以做一些相应的处理,比如打印日志、关闭链接
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
        extServerService.getRedisUtil().remove(ctx.channel().id().toString());
        CacheUtil.cacheChannel.remove(ctx.channel().id().toString(), ctx.channel());
        System.out.println("异常信息:\r\n" + cause.getMessage());
    }

}

CacheUtil.java | 缓存必要信息,用于业务流程处理

package com.lm.demo.netty.util;

import com.lm.demo.netty.domain.ServerInfo;
import com.lm.demo.netty.server.NettyServer;
import io.netty.channel.Channel;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;


public class CacheUtil {

    // 缓存channel
    public static Map<String, Channel> cacheChannel = Collections.synchronizedMap(new HashMap<String, Channel>());

    // 缓存服务信息
    public static Map<Integer, ServerInfo> serverInfoMap = Collections.synchronizedMap(new HashMap<Integer, ServerInfo>());

    // 缓存服务端
    public static Map<Integer, NettyServer> serverMap = Collections.synchronizedMap(new HashMap<Integer, NettyServer>());

}

package com.lm.demo.netty.web;

import com.alibaba.fastjson.JSON;
import com.lm.demo.netty.domain.EasyResult;
import com.lm.demo.netty.domain.ServerInfo;
import com.lm.demo.netty.domain.UserChannelInfo;
import com.lm.demo.netty.redis.RedisUtil;
import com.lm.demo.netty.server.NettyServer;
import com.lm.demo.netty.service.ExtServerService;
import com.lm.demo.netty.util.CacheUtil;
import com.lm.demo.netty.util.NetUtil;
import io.netty.channel.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;
import java.net.InetSocketAddress;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;


@Controller
public class NettyController {

    private Logger logger = LoggerFactory.getLogger(NettyController.class);
    //默认线程池
    private static ExecutorService executorService = Executors.newFixedThreadPool(2);

    @Value("${server.port}")
    private int serverPort;
    @Autowired
    private ExtServerService extServerService;
    @Resource
    private RedisUtil redisUtil;
    //Netty服务端
    private NettyServer nettyServer;

    @RequestMapping("/index")
    public String index(Model model) {
        model.addAttribute("serverPort", serverPort);
        return "index";
    }

    @RequestMapping("/openNettyServer")
    @ResponseBody
    public EasyResult openNettyServer() {
        try {
            int port = NetUtil.getPort();
            logger.info("启动Netty服务,获取可用端口:{}", port);
            nettyServer = new NettyServer(new InetSocketAddress(port), extServerService);
            Future<Channel> future = executorService.submit(nettyServer);
            Channel channel = future.get();
            if (null == channel) {
                throw new RuntimeException("netty server open error channel is null");
            }
            while (!channel.isActive()) {
                logger.info("启动Netty服务,循环等待启动...");
                Thread.sleep(500);
            }
            CacheUtil.serverInfoMap.put(port, new ServerInfo(NetUtil.getHost(), port, new Date()));
            CacheUtil.serverMap.put(port, nettyServer);
            logger.info("启动Netty服务,完成:{}", channel.localAddress());
            return EasyResult.buildSuccessResult();
        } catch (Exception e) {
            logger.error("启动Netty服务失败", e);
            return EasyResult.buildErrResult(e);
        }
    }

    @RequestMapping("/closeNettyServer")
    @ResponseBody
    public EasyResult closeNettyServer(int port) {
        try {
            logger.info("关闭Netty服务开始,端口:{}", port);
            NettyServer nettyServer = CacheUtil.serverMap.get(port);
            if (null == nettyServer) {
                CacheUtil.serverMap.remove(port);
                return EasyResult.buildSuccessResult();
            }
            nettyServer.destroy();
            CacheUtil.serverMap.remove(port);
            CacheUtil.serverInfoMap.remove(port);
            logger.info("关闭Netty服务完成,端口:{}", port);
            return EasyResult.buildSuccessResult();
        } catch (Exception e) {
            logger.error("关闭Netty服务失败,端口:{}", port, e);
            return EasyResult.buildErrResult(e);
        }
    }

    @RequestMapping("/queryNettyServerList")
    @ResponseBody
    public Collection<ServerInfo> queryNettyServerList() {
        try {
            Collection<ServerInfo> serverInfos = CacheUtil.serverInfoMap.values();
            logger.info("查询服务端列表。{}", JSON.toJSONString(serverInfos));
            return serverInfos;
        } catch (Exception e) {
            logger.info("查询服务端列表失败。", e);
            return null;
        }
    }

    @RequestMapping("/queryUserChannelInfoList")
    @ResponseBody
    public List<UserChannelInfo> queryUserChannelInfoList() {
        try {
            logger.info("查询用户列表信息开始");
            List<UserChannelInfo> userChannelInfoList = redisUtil.popList();
            logger.info("查询用户列表信息完成。list:{}", JSON.toJSONString(userChannelInfoList));
            return userChannelInfoList;
        } catch (Exception e) {
            logger.error("查询用户列表信息失败", e);
            return null;
        }
    }

}

resources/application.yml | 基础配置,在我们启动服务端的时候,如果只有一台机器模拟,那么需要改变server.port端口{8080、8081}

server:
  port: 8081

spring:
  mvc:
    view:
      prefix: /WEB-INF/
      suffix: .jsp
  redis:
    host: 127.0.0.1
    port: 6379

index.jsp | 页面操作,控制和展示的一些内容

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<title>关注明哥 | 专题案例开发,关注取源码 </title>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="res/js/themes/default/easyui.css">
<link rel="stylesheet" type="text/css" href="res/js/themes/icon.css">
<script type="text/javascript" src="res/js/jquery.min.js"></script>
<script type="text/javascript" src="res/js/jquery.easyui.min.js"></script>

<style>

</style>

<script>
util = {
  formatDate: function (value, row, index) {
      if (null == value) return "";
      var date = new Date();
      date.setTime(value);
      return date.format('yyyy-MM-dd HH:mm:ss');
  }
};
</script>

</head>

<body>
	<div style="margin:20px 0;"></div>
	<table class="easyui-datagrid" title="localhost:${serverPort} | Netty服务端" style="width:700px;height:250px"
			data-options="rownumbers:true,singleSelect:true,url:'/queryNettyServerList',method:'get',toolbar:toolbar">
		<thead>
			<tr>
				<th data-options="field:'ip'">IP</th>
				<th data-options="field:'port'">端口</th>
				<th data-options="field:'openDate'">启动时间</th>
			</tr>
		</thead>
	</table>
	<script type="text/javascript">
		var toolbar = [{
			text:'启动',
			iconCls:'icon-open',
			handler:function(){
			    $.post('/openNettyServer',{}, function (res) {
                               if (res.success) {
                                  $.messager.show({
                                        title: '消息提示',
                                        msg: '启动成功,请稍后刷新页面!'
                                  });
                                   $('#easyui-datagrid').datagrid('reload');
                               } else {
                                   $.messager.show({
                                       title: 'Error',
                                       msg: res.msg
                                   });
                               }
                            }, 'json');
			}
		},'-',{
			text:'关闭',
			iconCls:'icon-close',
			handler:function(){
			     //可以自己添加实现
			}
		}];
	</script>
    <hr/>
	<!-- server-user -->
    <table class="easyui-datagrid" title="localhost:${serverPort} | 用户链接信息" style="width:700px;height:250px"
    			data-options="rownumbers:true,singleSelect:true,url:'/queryUserChannelInfoList',method:'get'">
    		<thead>
    			<tr>
    				<th data-options="field:'ip'">IP</th>
    				<th data-options="field:'port'">端口</th>
    				<th data-options="field:'channelId'">用户ID</th>
    				<th data-options="field:'linkDate'">链接时间</th>
    			</tr>
    		</thead>
    </table>
</body>
</html>

测试结果
启动Redis服务
在这里插入图片描述
1:启动2次SpringBoot,模拟Netty集群[不同端口8080、8081] | Plugins/spring-boot/run 双击启动

2:启动2个以上的NetAssist分别链接到不同的服务端,以模拟测试跨服务通信,最后在客户端发送消息传递给另外一个不在本服务端的客户端。
最终运行效果
在这里插入图片描述
好了到这里就结束了netty之Netty集群部署实现跨服务端通信的落地方案的学习,大家一定要跟着动手操作起来。需要的源码的 可si我获取;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2188389.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【PS】删除自定义形状,添加自定义形状

删除自定义形状 在这里选择删除形状为灰色的时候&#xff0c;是不能直接删除的&#xff0c;需要打开形状窗口后才能删除。 找到形状窗口&#xff0c;打开它 然后就可以删除形状了。 导入形状 右键&#xff0c;导入形状 选择你要导入的形状包&#xff08;我这个是某宝买…

Stable Diffusion绘画 | 来训练属于自己的模型:秋叶训练器使用

花了不少时间搜索尝试&#xff0c;都没有找到解决上一篇文章遗留问题的解决方案&#xff0c;导致无法使用 cybertronfurnace 这个工具来完成炼丹&#xff0c;看不到炼丹效果。 但考虑到&#xff0c;以后还是要训练自己的模型&#xff0c; 于是决定放弃 cybertronfurnace&…

数据结构与算法——Java实现 28.二叉树的锯齿形层序遍历

努力成为你想要成为的那种人&#xff0c;去奔赴你想要的生活 —— 24.10.4 103. 二叉树的锯齿形层序遍历 给你二叉树的根节点 root &#xff0c;返回其节点值的 锯齿形层序遍历 。&#xff08;即先从左往右&#xff0c;再从右往左进行下一层遍历&#xff0c;以此类推&#xff…

【Unity】双摄像机叠加渲染

一、前言 之前我在做我的一个Unity项目的时候&#xff0c;需要绘制场景网格的功能&#xff0c;于是就用到了UnityEngine.GL这个图形库来绘制&#xff0c;然后我发现绘制的网格线是渲染在UI之后的&#xff0c;也就是说绘制出来的图形会遮盖在UI上面&#xff0c;也就导致一旦这些…

第十八章(数据在内存中的储存)

1. 整数在内存中的存储 2. ⼤⼩端字节序和字节序判断 3. 浮点数在内存中的存储 我本将心向明月&#xff0c;奈何明月照沟渠正文开始 一、.整数在内存中的储存 整数的2进制的表示方法有三种 1.原码 2.反码 3.补码 这里在第十章我们有详细讲解&#xff0c;有需要的同学可以自…

网络编程项目框架内容

基于TCP的云端书阅管理系统 通过网络实现图书借阅网站&#xff0c;包括服务器与客户端&#xff0c;客户端与服务器是基于TCP连接。 客户端描述&#xff1a;客户端运行会与服务器端进行连接&#xff0c;连接成功后&#xff0c;显示注册登录界面。此时&#xff0c;客户端可以选…

算法: FriendShip - Kruskal+并查集判环

题目 A-Friendship_2024.5.7 (nowcoder.com) 思路分析 求所有符合题意情况的最大值中的最小值&#xff1b;符合题意是指保证图的连通性。那么贪心思路&#xff0c;将所有已存在的关系和可能存在的关系存储起来&#xff0c;利用Kruskal贪心算法每次取权值最小的且不构成回路的…

从零开始讲PCIe(2)——PCI总线传输模型与机制

一、前言 在之前的内容中&#xff0c;我们已经对PCI有了一些基本的认识&#xff0c;我们了解了PCI的一般架构&#xff0c;标准传输周期等相关的内容&#xff0c;接下来我们会进一步了解PCI具体的传输模型和传输机制。 二、PCI传输模型 PCI一共有三种数据传输模型&#xff0c;分…

Windows安装ollama和AnythingLLM

1、Ollama安装部署 1&#xff09;安装ollama 官网下载&#xff1a;https://ollama.com/download&#xff0c;很慢 阿里云盘下载&#xff1a;https://www.alipan.com/s/jiwVVjc7eYb 提取码: ft90 百度云盘下载&#xff1a;https://pan.baidu.com/s/1o1OcY0FkycxMpZ7Ho8_5oA?…

Python-初识Python

前言&#xff1a;在这篇博客当中&#xff0c;我们将步入Python知识的殿堂&#xff0c;Python以其简单、易学、开发效率高在近些年的发展可谓是迅猛&#xff0c;在许多领域都可以见到它的场景&#xff0c;例如&#xff1a;人工智能/机器学习、大数据开发、后端开发等都会用到。 …

仕考网:公务员国考有三不限岗位吗?

国家公务员考试中的“三不限”岗位&#xff0c;即不限制专业背景、政治面貌、基层工作经验的职位。在国考中&#xff0c;是有的但是数量比较少。 这些岗位主要集中在省级及以下单位&#xff0c;以民航空警和铁路公安为主。其中&#xff0c;有一半的职位是面向四项目人员&#…

基于STM32的蓝牙音乐播放器设计

引言 本项目将基于STM32微控制器设计一个简易的蓝牙音乐播放器&#xff0c;通过蓝牙模块接收手机的音乐信号&#xff0c;并使用音频解码芯片播放音乐。该项目展示了STM32在嵌入式音频处理与蓝牙通信方面的应用。 环境准备 1. 硬件设备 STM32F103C8T6 开发板&#xff08;或其…

基于Java,SpringBoot,Vue智慧校园健康驿站体检论坛请假管理系统

摘要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对信息管理混乱&#xff0c;出错率高&#xff0c;信息安全性差&#xf…

持续更新:当前最好用的AI 编程工具,Cursor 编程指南

本文持续更新&#xff0c;敬请期待更多内容。 文章目录 这一次&#xff0c;AI真懂你的代码关注该关注的&#xff0c;忽略该忽略的1. 创建.cursorignore文件2. 重新索引代码库 参考资料 这一次&#xff0c;AI真懂你的代码 如果你偶尔关注一些AI编程相关的内容&#xff0c;想必你…

介绍多环境开发-分组(springboot-profile)

背景 在使用 Spring Boot 进行开发时&#xff0c;多环境配置是一项非常常见的需求。通常&#xff0c;我们会在开发、测试、生产等不同环境下部署同一个应用程序&#xff0c;而这些环境可能需要不同的配置&#xff0c;例如数据库连接、日志级别等。Spring Boot 通过 profile&am…

python交互式命令时如何清除

在交互模式中使用Python&#xff0c;如果要清屏&#xff0c;可以import os&#xff0c;通过os.system()来调用系统命令clear或者cls来实现清屏。 [python] view plain copy print? >>> import os >>> os.system(clear) 但是此时shell中的状态是&#xff1a;…

windows的一些容易忽视的使用记录

文章目录 快捷键更改电脑名字共享文件夹添加新账号&#xff08;本地的&#xff09;更改快捷访问 以下都基于 win 10。 快捷键 win I 直接打开设置。 win R 打开运行栏。这个非常常用。 更改电脑名字 先 win I 打开设置&#xff0c;然后点击系统。 左侧栏拉到最下面&…

Android开发高级篇:MVVM框架与数据双向绑定

在Android开发中&#xff0c;MVVM&#xff08;Model-View-ViewModel&#xff09;架构模式以其高效、简洁的特点&#xff0c;成为越来越多开发者的首选。MVVM不仅实现了界面&#xff08;UI&#xff09;与业务逻辑的分离&#xff0c;还通过数据双向绑定技术&#xff0c;极大地简化…

iterator的使用+求数组中的第n大值+十大经典排序算法

目录 一、iterator的用法 二、求一个数组中的第n大值&#xff08;n为2或者3&#xff09; 1、求一个数组中的第二大值&#xff08;不能使用排序&#xff09; 2、求一个数组中的第三大值&#xff08;不能使用排序&#xff09; 三、冒泡排序 1、基本思想 2、代码实现 3、存…

AQS原理(AbstractQueuedSynchronizer)

本篇为 [并发与多线程系列] 的第四篇&#xff0c;对应Java知识体系脑图中的 并发与多线程 模块。 这一系列将对Java中并发与多线程的内容来展开。 AQS原理&#xff08;AbstractQueuedSynchronizer&#xff09; AQS原理&#xff08;AbstractQueuedSynchronizer&#xff09;AQS整…