当前位置: 首页 > news >正文

Springboot 中使用 Redisson+AOP+自定义注解 实现访问限流与黑名单拦截

🏷️个人主页:牵着猫散步的鼠鼠 

🏷️系列专栏:Java全栈-专栏

🏷️个人学习笔记,若有缺误,欢迎评论区指正 

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站AI学习网站。

目录

前言

1.导入Redisson

引入依赖

编写配置

声明Redisson客户端Bean

2.自定义注解

3.AOP切面编程

导入依赖

编写AOP限流代码

4.接口使用自定义注解实现限流

使用自定义限流注解

绑定限流回调函数

总结


前言

在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。  限流的目的是通过对并发访问请求进行限速或者一个时间窗口内的的请求数量进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待

我们上次讲解了如何使用Sentinel来实现服务限流,今天我们来讲解下如何使用Redisson+AOP+自定义注解+反射优雅的实现服务限流,本文讲解的限流实现支持针对用户IP限流,整个接口的访问限流,以及对某个参数字段的限流,并且支持请求限流后处理回调

1.导入Redisson

引入依赖

我们首先导入Redisson所需要的依赖,我们这里的springboot版本为2.7.12

<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.23.4</version>
</dependency>

编写配置

# Redisson客户端
redis:sdk:config:host: redis服务IPport: 6379password: redis密码,没有可删掉这行pool-size: 10min-idle-size: 5idle-timeout: 30000connect-timeout: 5000retry-attempts: 3retry-interval: 1000ping-interval: 60000keep-alive: true

声明Redisson客户端Bean

配置映射类RedisCientConfigProperties

@Data
@ConfigurationProperties(prefix = "redis.sdk.config", ignoreInvalidFields = true)
public class RedisCientConfigProperties {/** host:ip */private String host;/** 端口 */private int port;/** 账密 */private String password;/** 设置连接池的大小,默认为64 */private int poolSize = 64;/** 设置连接池的最小空闲连接数,默认为10 */private int minIdleSize = 10;/** 设置连接的最大空闲时间(单位:毫秒),超过该时间的空闲连接将被关闭,默认为10000 */private int idleTimeout = 10000;/** 设置连接超时时间(单位:毫秒),默认为10000 */private int connectTimeout = 10000;/** 设置连接重试次数,默认为3 */private int retryAttempts = 3;/** 设置连接重试的间隔时间(单位:毫秒),默认为1000 */private int retryInterval = 1000;/** 设置定期检查连接是否可用的时间间隔(单位:毫秒),默认为0,表示不进行定期检查 */private int pingInterval = 0;/** 设置是否保持长连接,默认为true */private boolean keepAlive = true;
}
Configuration
@EnableConfigurationProperties(RedisCientConfigProperties.class)
public class RedisClientConfig {@Bean("redissonClient")public RedissonClient redissonClient(ConfigurableApplicationContext applicationContext, RedisCientConfigProperties properties) {Config config = new Config();// 根据需要可以设定编解码器;https://github.com/redisson/redisson/wiki/4.-%E6%95%B0%E6%8D%AE%E5%BA%8F%E5%88%97%E5%8C%96// config.setCodec(new RedisCodec());config.useSingleServer().setAddress("redis://" + properties.getHost() + ":" + properties.getPort()).setPassword(properties.getPassword()).setConnectionPoolSize(properties.getPoolSize()).setConnectionMinimumIdleSize(properties.getMinIdleSize()).setIdleConnectionTimeout(properties.getIdleTimeout()).setConnectTimeout(properties.getConnectTimeout()).setRetryAttempts(properties.getRetryAttempts()).setRetryInterval(properties.getRetryInterval()).setPingConnectionInterval(properties.getPingInterval()).setKeepAlive(properties.isKeepAlive());RedissonClient redissonClient = Redisson.create(config);// 注册消息发布订阅主题Topic// 找到所有实现了Redisson中MessageListener接口的bean名字String[] beanNamesForType = applicationContext.getBeanNamesForType(MessageListener.class);for (String beanName : beanNamesForType) {// 通过bean名字获取到监听beanMessageListener bean = applicationContext.getBean(beanName, MessageListener.class);Class<? extends MessageListener> beanClass = bean.getClass();// 如果bean的注解里包含我们的自定义注解RedisTopic.class,则以RedisTopic注解的值作为name将该bean注册到bean工厂,方便在别处注入if (beanClass.isAnnotationPresent(RedisTopic.class)) {RedisTopic redisTopic = beanClass.getAnnotation(RedisTopic.class);RTopic topic = redissonClient.getTopic(redisTopic.topic());topic.addListener(String.class, bean);ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();beanFactory.registerSingleton(redisTopic.topic(), topic);}}return redissonClient;}static class RedisCodec extends BaseCodec {private final Encoder encoder = in -> {ByteBuf out = ByteBufAllocator.DEFAULT.buffer();try {ByteBufOutputStream os = new ByteBufOutputStream(out);JSON.writeJSONString(os, in, SerializerFeature.WriteClassName);return os.buffer();} catch (IOException e) {out.release();throw e;} catch (Exception e) {out.release();throw new IOException(e);}};private final Decoder<Object> decoder = (buf, state) -> JSON.parseObject(new ByteBufInputStream(buf), Object.class);@Overridepublic Decoder<Object> getValueDecoder() {return decoder;}@Overridepublic Encoder getValueEncoder() {return encoder;}}}

2.自定义注解

我们这里自定义一个注解来作为后续AOP切面编程的切点

根据注解Key属性的值,我们会有如下情况

all:针对整个接口限流

request_ip:针对各个用户的访问IP限流

其他str:根据参数作为标识符限流,比如我这里key=userid,那么我会根据参数中的userid来限流

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface AccessInterceptor {/** 用哪个字段作为拦截标识符,配置all则是对整个接口限流,配置request_ip,* 则是对访问ip限流,配置其他str,则会到参数中寻找对应名称的属性值(包括对象内部属性) */String key() default "all";/** 限制频次(每秒请求次数) */long permitsPerSecond();/** 黑名单拦截(多少次限制后加入黑名单)0 不限制 */double blacklistCount() default 0;/** 拦截后的执行方法 */String fallbackMethod();}

3.AOP切面编程

导入依赖

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

编写AOP限流代码

我们doRouter切面函数以AccessInterceptor注解为切点,根据注解的各类配置来执行整个限流过程。

我们通过使用Redisson的RRateLimiter限流器,基于令牌桶实现访问限流,并对已经限流的访问记录黑名单次数,超过设置的黑名单阈值就会被加入黑名单中,较长时间无法访问

代码较长,为了缩短篇幅就一次性放上来了,各处已经打上了详细注解,若有疑问可评论区留言。

@Slf4j
@Aspect
public class RateLimiterAOP {// 注入我们声明的redisson客户端@Resourceprivate RedissonClient redissonClient;// 限流RateLimiter缓存前缀private static final String rateLimiterName = "test:RateLimiter:";// 黑名单原子计数器缓存前缀private static final String blacklistPrefix = "test:RateBlockList:";@Around("@annotation(accessInterceptor)")public Object doRouter(ProceedingJoinPoint jp, AccessInterceptor accessInterceptor) throws Throwable {// 获取注解配置的字段keyString key = accessInterceptor.key();if (StringUtils.isBlank(key)) {log.error("限流RateLimiter注解中的 Key 属性为空!");throw new RuntimeException("RateLimiter注解中的 Key 属性为空!");}log.info("限流拦截关键字为 {}", key);// 根据key获取拦截标识符字段String keyAttr = getAttrValue(key, jp.getArgs());// 黑名单拦截,非法访问次数超过黑名单阈值if (!"all".equals(keyAttr) && accessInterceptor.blacklistCount() != 0 && redissonClient.getAtomicLong(blacklistPrefix + keyAttr).get() > accessInterceptor.blacklistCount()) {log.info("限流-黑名单拦截:{}", keyAttr);return fallbackMethodResult(jp, accessInterceptor.fallbackMethod());}// 获取限流器 -> Redisson RRateLimiterRRateLimiter rateLimiter = redissonClient.getRateLimiter(rateLimiterName + keyAttr);if (!rateLimiter.isExists()) {// 创建令牌桶数据模型,单位时间内产生多少令牌rateLimiter.trySetRate(RateType.PER_CLIENT,1, accessInterceptor.permitsPerSecond(), RateIntervalUnit.MINUTES);}// 限流判断,没有获取到令牌,超出频率if (!rateLimiter.tryAcquire()) {// 如果开启了黑名单限制,那么就记录当前的非法访问次数if (accessInterceptor.blacklistCount() != 0) {RAtomicLong atomicLong = redissonClient.getAtomicLong(blacklistPrefix + keyAttr);atomicLong.incrementAndGet(); // 原子自增atomicLong.expire(24, TimeUnit.HOURS); // 刷新黑名单原子计数器器过期时间为24小时}log.info("限流-频率过高拦截:{}", keyAttr);return fallbackMethodResult(jp, accessInterceptor.fallbackMethod());}// 返回结果return jp.proceed();}/*** 调用用户配置的回调方法,使用反射机制实现。*/private Object fallbackMethodResult(JoinPoint jp, String fallbackMethod) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {// 通过JoinPoint对象获取方法的签名(Signature)Signature sig = jp.getSignature();// 将方法签名转换为MethodSignature对象,以便获取方法的详细信息MethodSignature methodSignature = (MethodSignature) sig;// 获取到具体的方法对象,通过方法名和参数(所以回调函数参数一定要和原方法一致)Method method = jp.getTarget().getClass().getMethod(fallbackMethod, methodSignature.getParameterTypes());// 调用目标对象的方法,并传入当前对象(jp.getThis())和方法的参数(jp.getArgs())。return method.invoke(jp.getThis(), jp.getArgs());}/*** 根据JoinPoint对象获取其所代表的方法对象*/private Method getMethod(JoinPoint jp) throws NoSuchMethodException {Signature sig = jp.getSignature();MethodSignature methodSignature = (MethodSignature) sig;return jp.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());}/*** 实际根据自身业务调整,主要是为了获取通过某个值做拦截*/public String getAttrValue(String attr, Object[] args) {String filedValue = null;for (Object arg : args) {try {// 找到HttpServletRequest对象来获取请求IP地址(如果是根据IP拦截的话)if ("request_ip".equals(attr) && arg instanceof HttpServletRequest) {HttpServletRequest request = (HttpServletRequest) arg;filedValue = IPUtils.getIpAddr(request);}// 找到了值,返回if (StringUtils.isNotBlank(filedValue)) {break;}// fix: 使用lombok时,uId这种字段的get方法与idea生成的get方法不同,会导致获取不到属性值,改成反射获取解决filedValue = String.valueOf(this.getValueByName(arg, attr));} catch (Exception e) {log.error("获取路由属性值失败 attr:{}", attr, e);}}return filedValue;}/*** 获取对象的特定属性值(反射)** @param item 对象* @param name 属性名* @return 属性值* @author tang*/private Object getValueByName(Object item, String name) {try {// 获取指定对象中对应属性名的Field对象Field field = getFieldByName(item, name);// 获取到的Field对象为null,表示属性不存在,直接返回null。if (field == null) {return null;}// 将Field对象设置为可访问,以便获取私有属性的值。field.setAccessible(true);// 获取属性值,并将其赋值给变量o。Object o = field.get(item);// 将Field对象设置为不可访问,以保持对象的封装性。field.setAccessible(false);return o;} catch (IllegalAccessException e) {return null;}}/*** 根据名称获取方法,该方法同时兼顾继承类获取父类的属性** @param item 对象* @param name 属性名* @return 该属性对应方法* @author tang*/private Field getFieldByName(Object item, String name) {try {Field field;try {// 获取指定对象中对应属性名的Field对象。field = item.getClass().getDeclaredField(name);} catch (NoSuchFieldException e) {// 没有找到,抛出NoSuchFieldException异常,尝试获取父类中对应属性名的Field对象field = item.getClass().getSuperclass().getDeclaredField(name);}return field;} catch (NoSuchFieldException e) {// 父类也没找到对应属性名的Field对象,寄,返回nullreturn null;}}}

以上代码用到了自己写的一个工具类IPUtils来获取请求的IP地址,内容如下

public class IPUtils {private static Logger logger = LoggerFactory.getLogger(IPUtils.class);private static final String IP_UTILS_FLAG = ",";private static final String UNKNOWN = "unknown";private static final String LOCALHOST_IP = "0:0:0:0:0:0:0:1";private static final String LOCALHOST_IP1 = "127.0.0.1";/*** 获取IP地址* <p>* 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址*/public static String getIpAddr(HttpServletRequest request) {String ip = null;try {//以下两个获取在k8s中,将真实的客户端IP,放到了x-Original-Forwarded-For。而将WAF的回源地址放到了 x-Forwarded-For了。ip = request.getHeader("X-Original-Forwarded-For");if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getHeader("X-Forwarded-For");}//获取nginx等代理的ipif (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getHeader("x-forwarded-for");}if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getHeader("Proxy-Client-IP");}if (StringUtils.isEmpty(ip) || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getHeader("WL-Proxy-Client-IP");}if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getHeader("HTTP_CLIENT_IP");}if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getHeader("HTTP_X_FORWARDED_FOR");}//兼容k8s集群获取ipif (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getRemoteAddr();if (LOCALHOST_IP1.equalsIgnoreCase(ip) || LOCALHOST_IP.equalsIgnoreCase(ip)) {//根据网卡取本机配置的IPInetAddress iNet = null;try {iNet = InetAddress.getLocalHost();} catch (UnknownHostException e) {logger.error("getClientIp error: {}", e);}ip = iNet.getHostAddress();}}} catch (Exception e) {logger.error("IPUtils ERROR ", e);}//使用代理,则获取第一个IP地址if (!StringUtils.isEmpty(ip) && ip.indexOf(IP_UTILS_FLAG) > 0) {ip = ip.substring(0, ip.indexOf(IP_UTILS_FLAG));}return ip;}}

4.接口使用自定义注解实现限流

使用自定义限流注解

比如我在用户controller层的登录接口上使用注解,key为request_ip,表示根据用户IP限流,回调函数为fallbackMethod,每分钟访问限制10次

    @PostMapping(value = "/login")@AccessInterceptor(key = "request_ip", fallbackMethod = "loginErr", permitsPerSecond = 1L, blacklistCount = 10)public Response<String> doLogin(@RequestParam String code, HttpServletRequest request){

绑定限流回调函数

这里需要注意的是,回调函数的参数必须和你使用限流注解的方法参数一致,否则报对应方法找不到的错误(因为这里是通过反射机制找到回调函数执行的)

public Response<String> loginErr(String code, HttpServletRequest request) {System.out.println("限流触发回调,参数信息:" + code);return Response.<String>builder().code(Constants.ResponseCode.FREQUENCY_LIMITED.getCode()).info(Constants.ResponseCode.FREQUENCY_LIMITED.getInfo()).data(code).build();}

总结

以上通过Redission+自定义注解+AOP+反射实现了对不同标识符的限流和黑名单拦截,并且可以绑定限流回调函数来处理限流后的逻辑,代码篇幅较长,各位小伙伴也可以尝试继续优化一下这里的设计,减少request_ip这种魔法值(实在懒得改了),感谢您的收看,万字长文(虽然大部分是代码),有帮助就多多支持吧

相关文章:

Springboot 中使用 Redisson+AOP+自定义注解 实现访问限流与黑名单拦截

&#x1f3f7;️个人主页&#xff1a;牵着猫散步的鼠鼠 &#x1f3f7;️系列专栏&#xff1a;Java全栈-专栏 &#x1f3f7;️个人学习笔记&#xff0c;若有缺误&#xff0c;欢迎评论区指正 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&…...

Java使用企业邮箱发送预警邮件

前言&#xff1a;最近接到一个需求&#xff0c;需要根据所监控设备的信息&#xff0c;在出现问题时发送企业微信进行预警。 POM依赖 <!-- 邮件 --> <dependency><groupId>com.sun.mail</groupId><artifactId>jakarta.mail</artifactId>…...

Unity编辑器扩展之是否勾选Text组件BestFit选项工具(此篇教程也可以操作其他组件的属性)

想要批量化是否勾选项目预制体资源中Text组件BestFit属性&#xff08;此篇教程也可以操作其他组件的属性&#xff0c;只不过需要修改其中对应的代码&#xff09;&#xff0c;可以采用以下步骤。 1、在项目的Editor文件中&#xff0c;新建一个名为TextBestFitBatchProcessor的…...

分布式场景怎么Join | 京东云技术团队

背景 最近在阅读查询优化器的论文&#xff0c;发现System R中对于Join操作的定义一般分为了两种&#xff0c;即嵌套循环、排序-合并联接。在原文中&#xff0c;更倾向使用排序-合并联接逻辑。 考虑到我的领域是在处理分库分表或者其他的分区模式&#xff0c;这让我开始不由得…...

24-k8s的附件组件-Metrics-server组件与hpa资源pod水平伸缩

一、概述 Metrics-Server组件目的&#xff1a;获取集群中pod、节点等负载信息&#xff1b; hpa资源目的&#xff1a;通过metrics-server获取的pod负载信息&#xff0c;自动伸缩创建pod&#xff1b; 参考链接&#xff1a; 资源指标管道 | Kubernetes https://github.com/kuberne…...

Spring RabbitMQ 配置多个虚拟主机(vhost)

文章目录 前言一、相关文章二、相关代码1.yml文件配置2.RabbitMq配置类3.接收MQ消息前言 在日常开发中,同时需要用到RabbitMQ多个虚拟机(vhost)。应用场景:需要接收多个交换机的数据,而交换机都在不同的虚拟机(vhost) 一、相关文章 Docker安装RabbitMQ 【SpringCloud…...

「Qt Widget中文示例指南」如何实现文档查看器?(一)

Qt 是目前最先进、最完整的跨平台C开发工具。它不仅完全实现了一次编写&#xff0c;所有平台无差别运行&#xff0c;更提供了几乎所有开发过程中需要用到的工具。如今&#xff0c;Qt已被运用于超过70个行业、数千家企业&#xff0c;支持数百万设备及应用。 文档查看器是一个显…...

如何创建WordPress付款表单(简单方法)

您是否正在寻找一种简单的方法来创建付款功能WordPress表单&#xff1f; 小企业主通常需要创建一种简单的方法来在其网站上接受付款&#xff0c;而无需设置复杂的购物车。简单的付款表格使您可以轻松接受自定义付款金额、设置定期付款并收集自定义详细信息。 在本文中&#x…...

虹科方案 | 释放总线潜力:汽车总线离线模拟解决方案

来源&#xff1a;虹科汽车智能互联 虹科方案 | 释放总线潜力&#xff1a;汽车总线离线模拟解决方案 原文链接&#xff1a;https://mp.weixin.qq.com/s/KGv2ZOuQMLIXlOiivvY6aQ 欢迎关注虹科&#xff0c;为您提供最新资讯&#xff01; #汽车总线 #ECU #汽车网关 导读 传统的…...

欲速则不达,慢就是快!

引言 随着生活水平的提高&#xff0c;不少人的目标从原先的解决温饱转变为追求内心充实&#xff0c;但由于现在的时间过得越来越快以及其他外部因素&#xff0c;我们对很多东西的获取越来越没耐心&#xff0c;例如书店经常会看到《7天精通Java》、《3天掌握XXX》等等之类的书籍…...

ubuntu22.04@Jetson OpenCV安装

ubuntu22.04Jetson OpenCV安装 1. 源由2. 分析3. 证实3.1 jtop安装3.2 jtop指令3.3 GPU支持情况 4. 安装OpenCV4.1 修改内容4.2 Python2环境【不需要】4.3 ubuntu22.04环境4.4 国内/本地环境问题4.5 cudnn版本问题 5. 总结6. 参考资料 1. 源由 昨天用Jetson跑demo程序发现帧率…...

OpenGL学习——17.模型

前情提要&#xff1a;本文代码源自Github上的学习文档“LearnOpenGL”&#xff0c;我仅在源码的基础上加上中文注释。本文章不以该学习文档做任何商业盈利活动&#xff0c;一切著作权归原作者所有&#xff0c;本文仅供学习交流&#xff0c;如有侵权&#xff0c;请联系我删除。L…...

6.2 数据库

本节介绍Android的数据库存储方式--SQLite的使用方法&#xff0c;包括&#xff1a;SQLite用到了哪些SQL语法&#xff0c;如何使用数据库管理操纵SQLitem&#xff0c;如何使用数据库帮助器简化数据库操作&#xff0c;以及如何利用SQLite改进登录页面的记住密码功能。 6.2.1 SQ…...

计算机设计大赛 深度学习人体跌倒检测 -yolo 机器视觉 opencv python

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; **基于深度学习的人体跌倒检测算法研究与实现 ** 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f947;学长这里给一个题目综合评分(每项满…...

本地模拟发送、接收RabbitMQ数据

文章目录 前言一、相关文章二、相关代码1.模拟的 Channel 类2.接收消息3.模拟推送MQ数据前言 日常开发中,当线上RabbitMQ坏境还没准备好时,可在本地模拟发送、接收消息 一、相关文章 Docker安装RabbitMQ 【SpringCloud】整合RabbitMQ六大模式应用(入门到精通) Spring R…...

前端 webSocket 的使用

webSocket使用 注意要去监听websocket 对象事件&#xff0c;处理我们需要的数据 我是放在了最外层的index 内&#xff0c;监听编辑状态&#xff0c;去触发定义的方法。因为我这个项目是组件化开发&#xff0c;全部只有一个总编辑按钮&#xff0c;我只需监听是否触发了编辑即可…...

opencv图像处理(一)

一. OpenCV 简介 OpenCV 是一个跨平台计算机视觉库&#xff0c;可以运行在Linux、Windows、Android和Mac OS操作系统上。 应用领域 1、人机互动 2、物体识别 3、图像分割 4、人脸识别 5、动作识别 6、运动跟踪 7、机器人 8、运动分析 9、机器视觉 10、…...

消息队列-RabbitMQ:workQueues—工作队列、消息应答机制、RabbitMQ 持久化、不公平分发(能者多劳)

4、Work Queues Work Queues— 工作队列 (又称任务队列) 的主要思想是避免立即执行资源密集型任务&#xff0c;而不得不等待它完成。我们把任务封装为消息并将其发送到队列&#xff0c;在后台运行的工作进程将弹出任务并最终执行作业。当有多个工作线程时&#xff0c;这些工作…...

前端秘法基础式(HTML)(第二卷)

目录 一.表单标签 1.表单域 2.表单控件 2.1input标签 2.2label/select/textarea标签 2.3无语义标签 三.特殊字符 一.表单标签 用来完成与用户的交互,例如登录系统 1.表单域 <form>通过action属性,将用户填写的数据转交给服务器 2.表单控件 2.1input标签 type…...

PTA-统计英文字母和数字字符[2]

本题要求编写程序&#xff0c;输入N个字符&#xff0c;统计其中英文字母、数字字符和其他字符的个数。 输入格式: 输入在第一行中给出正整数N&#xff0c;第二行输入N个字符&#xff0c;最后一个回车表示输入结束&#xff0c;不算在内。 输出格式: 在一行内按照 letter 英…...

深入浅出Asp.Net Core MVC应用开发系列-AspNetCore中的日志记录

ASP.NET Core 是一个跨平台的开源框架&#xff0c;用于在 Windows、macOS 或 Linux 上生成基于云的新式 Web 应用。 ASP.NET Core 中的日志记录 .NET 通过 ILogger API 支持高性能结构化日志记录&#xff0c;以帮助监视应用程序行为和诊断问题。 可以通过配置不同的记录提供程…...

边缘计算医疗风险自查APP开发方案

核心目标:在便携设备(智能手表/家用检测仪)部署轻量化疾病预测模型,实现低延迟、隐私安全的实时健康风险评估。 一、技术架构设计 #mermaid-svg-iuNaeeLK2YoFKfao {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg…...

dedecms 织梦自定义表单留言增加ajax验证码功能

增加ajax功能模块&#xff0c;用户不点击提交按钮&#xff0c;只要输入框失去焦点&#xff0c;就会提前提示验证码是否正确。 一&#xff0c;模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...

多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验

一、多模态商品数据接口的技术架构 &#xff08;一&#xff09;多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如&#xff0c;当用户上传一张“蓝色连衣裙”的图片时&#xff0c;接口可自动提取图像中的颜色&#xff08;RGB值&…...

Spring Boot面试题精选汇总

&#x1f91f;致敬读者 &#x1f7e9;感谢阅读&#x1f7e6;笑口常开&#x1f7ea;生日快乐⬛早点睡觉 &#x1f4d8;博主相关 &#x1f7e7;博主信息&#x1f7e8;博客首页&#x1f7eb;专栏推荐&#x1f7e5;活动信息 文章目录 Spring Boot面试题精选汇总⚙️ **一、核心概…...

wpf在image控件上快速显示内存图像

wpf在image控件上快速显示内存图像https://www.cnblogs.com/haodafeng/p/10431387.html 如果你在寻找能够快速在image控件刷新大图像&#xff08;比如分辨率3000*3000的图像&#xff09;的办法&#xff0c;尤其是想把内存中的裸数据&#xff08;只有图像的数据&#xff0c;不包…...

ubuntu系统文件误删(/lib/x86_64-linux-gnu/libc.so.6)修复方案 [成功解决]

报错信息&#xff1a;libc.so.6: cannot open shared object file: No such file or directory&#xff1a; #ls, ln, sudo...命令都不能用 error while loading shared libraries: libc.so.6: cannot open shared object file: No such file or directory重启后报错信息&…...

6个月Python学习计划 Day 16 - 面向对象编程(OOP)基础

第三周 Day 3 &#x1f3af; 今日目标 理解类&#xff08;class&#xff09;和对象&#xff08;object&#xff09;的关系学会定义类的属性、方法和构造函数&#xff08;init&#xff09;掌握对象的创建与使用初识封装、继承和多态的基本概念&#xff08;预告&#xff09; &a…...

GraphQL 实战篇:Apollo Client 配置与缓存

GraphQL 实战篇&#xff1a;Apollo Client 配置与缓存 上一篇&#xff1a;GraphQL 入门篇&#xff1a;基础查询语法 依旧和上一篇的笔记一样&#xff0c;主实操&#xff0c;没啥过多的细节讲解&#xff0c;代码具体在&#xff1a; https://github.com/GoldenaArcher/graphql…...

阿里云Ubuntu 22.04 64位搭建Flask流程(亲测)

cd /home 进入home盘 安装虚拟环境&#xff1a; 1、安装virtualenv pip install virtualenv 2.创建新的虚拟环境&#xff1a; virtualenv myenv 3、激活虚拟环境&#xff08;激活环境可以在当前环境下安装包&#xff09; source myenv/bin/activate 此时&#xff0c;终端…...