shiro学习五:使用springboot整合shiro。在前面学习四的基础上,增加shiro的缓存机制,源码讲解:认证缓存、授权缓存。

news2025/2/3 6:48:09

文章目录

  • 前言
  • 1. 直接上代码最后在讲解
    • 1.1 新增的pom依赖
    • 1.2 RedisCache.java
    • 1.3 RedisCacheManager.java
    • 1.4 jwt的三个类
    • 1.5 ShiroConfig.java新增Bean
  • 2. 源码讲解。
    • 2.1 shiro 缓存的代码流程。
    • 2.2 缓存流程
      • 2.2.1 认证和授权简述
      • 2.2.2 AuthenticatingRealm.getAuthenticationInfo() 认证缓存。
      • 2.2.3 AuthorizingRealm.getAuthorizationInfo() 授权缓存。
    • 2.3 redis缓存的设置入口(个人认为是重点)
  • 3. 问题解决
    • 3.1 授权的流程是怎么做的
    • 3.2 @RequiresPermissions("sys:user:list")是如何工作的
      • **1. 基本原理**
      • **2. 具体流程**
        • **1. 用户登录阶段**
        • **2. 调用 `listUsers` 方法时**
        • **3. 匹配机制**

前言

  • shiro学习一:了解shiro,学习执行shiro的流程。使用springboot的测试模块学习shiro单应用(demo 6个)

  • shiro学习二:shiro的加密认证详解,加盐与不加盐两个版本。

  • shiro学习三:shiro的源码分析

  • 密码专辑:对密码加盐加密,对密码进行md5加密,封装成密码工具类

  • shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。

  • 代码所在地:https://github.com/fengfanli/springboot-shiro 记得给个星哦。

  • 本文详细介绍了在Java Spring Boot项目中使用Apache Shiro进行权限管理的实现方式。通过整合Redis作为缓存管理器,实现了用户权限的缓存,提高了权限验证的效率。文章还解析了@RequiresPermissions注解的工作原理,说明了如何通过AOP机制拦截方法,并进行权限字符串的匹配校验,确保只有具备相应权限的用户能够访问受保护的方法。整体上,文章为Shiro在Spring Boot项目中的应用提供了全面的技术指导。

1. 直接上代码最后在讲解

1.1 新增的pom依赖

<!--JWT-->
 <dependency>
     <groupId>io.jsonwebtoken</groupId>
     <artifactId>jjwt</artifactId>
     <version>0.9.1</version>
 </dependency>

1.2 RedisCache.java

package com.feng.shiro;

import com.alibaba.fastjson.JSON;
import com.feng.constant.Constant;
import com.feng.jwt.JwtTokenUtil;
import com.feng.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.util.CollectionUtils;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @ClassName: RedisCache
 * @Description: 缓存工具类
 * @createTime: 2020/2/9 20:42
 * @Author: 冯凡利
 * @UpdateUser: 冯凡利
 * @Version: 0.0.1
 */

/**
 * 这里的 RedisUtils 不能注入
 *
 * @param <K>
 * @param <V>
 */
@Slf4j
public class RedisCache<K, V> implements Cache<K, V> {

    private final static String PREFIX = "shiro-cache:";
    private String cacheKey;
    private long expire = 24;  // 24 小时

    private RedisUtil redisUtil;

    public RedisCache(String name, RedisUtil redisUtil) {
//        this.cacheKey=PREFIX+name+":";
        this.cacheKey = Constant.IDENTIFY_CACHE_KEY;
        this.redisUtil = redisUtil;
    }


    /**
     * 根据 key 值 获取 权限信息
     *
     * @param key  jwt
     * @return
     * @throws CacheException
     */
    @Override
    public V get(K key) throws CacheException {
        log.info("Shiro 从缓存中获取数据 KEY 值[{}]", key);
        if (key == null) {
            return null;
        }
        try {
            String redisCacheKey = getRedisCacheKey(key);
            Object rawValue = redisUtil.get(redisCacheKey);  // 根据key 获取 数据
            if (rawValue == null) {
                return null;
            }
            SimpleAuthorizationInfo info = JSON.parseObject(rawValue.toString(), SimpleAuthorizationInfo.class);
            V value = (V) info;
            return value;
        } catch (Exception e) {
            throw new CacheException(e);
        }
    }

    /**
     * 存值
     *
     * @param key jwt
     * @param value
     * @return
     * @throws CacheException
     */
    @Override
    public V put(K key, V value) throws CacheException {
        log.info("put key [{}]", key);
        if (key == null) {
            log.warn("Saving a null key is meaningless, return value directly without call Redis.");
            return value;
        }
        try {
            String redisCacheKey = getRedisCacheKey(key); // cacheKey + userId
            redisUtil.set(redisCacheKey, value != null ? value : null, expire, TimeUnit.HOURS);
            return value;
        } catch (Exception e) {
            throw new CacheException(e);
        }
    }

    /**
     * 根据 key 值 删除缓存的值
     *
     * @param key
     * @return
     * @throws CacheException
     */
    @Override
    public V remove(K key) throws CacheException {
        log.info("remove key [{}]", key);
        if (key == null) {
            return null;
        }
        try {
            String redisCacheKey = getRedisCacheKey(key);
            Object rawValue = redisUtil.get(redisCacheKey);
            V previous = (V) rawValue;
            redisUtil.delete(redisCacheKey);
            return previous;
        } catch (Exception e) {
            throw new CacheException(e);
        }
    }

    /**
     * 清除 所有的值
     *
     * @throws CacheException
     */
    @Override
    public void clear() throws CacheException {
        log.debug("clear cache");
        Set<String> keys = null;
        try {
            keys = redisUtil.keys(this.cacheKey + "*");
        } catch (Exception e) {
            log.error("get keys error", e);
        }
        if (keys == null || keys.size() == 0) {
            return;
        }
        for (String key : keys) {
            redisUtil.delete(key);
        }
    }

    /**
     * 获取 redis 所存的 缓存数的大小
     *
     * @return
     */
    @Override
    public int size() {
        int result = 0;
        try {
            result = redisUtil.keys(this.cacheKey + "*").size();
        } catch (Exception e) {
            log.error("get keys error", e);
        }
        return result;
    }

    /**
     * 获取key值
     *
     * @return
     */
    @SuppressWarnings("unchecked")
    @Override
    public Set<K> keys() {
        Set<String> keys = null;
        try {
            keys = redisUtil.keys(this.cacheKey + "*");
        } catch (Exception e) {
            log.error("get keys error", e);
            return Collections.emptySet();
        }
        if (CollectionUtils.isEmpty(keys)) {
            return Collections.emptySet();
        }
        Set<K> convertedKeys = new HashSet<>();
        for (String key : keys) {
            try {
                convertedKeys.add((K) key);
            } catch (Exception e) {
                log.error("deserialize keys error", e);
            }
        }
        return convertedKeys;
    }

    /**
     * 获取 value值
     *
     * @return
     */
    @Override
    public Collection<V> values() {
        Set<String> keys = null;
        try {
            keys = redisUtil.keys(this.cacheKey + "*");
        } catch (Exception e) {
            log.error("get values error", e);
            return Collections.emptySet();
        }
        if (CollectionUtils.isEmpty(keys)) {
            return Collections.emptySet();
        }
        List<V> values = new ArrayList<V>(keys.size());
        for (String key : keys) {
            V value = null;
            try {
                value = (V) redisUtil.get(key);
            } catch (Exception e) {
                log.error("deserialize values= error", e);
            }
            if (value != null) {
                values.add(value);
            }
        }
        return Collections.unmodifiableList(values);
    }

    /**
     * 获取 redis 中的 缓存 key  ,很重要,
     *
     * @param key
     * @return
     */
    private String getRedisCacheKey(K key) {
        if (null == key) {
            return null;
        } else {
            return this.cacheKey + JwtTokenUtil.getUserId(key.toString());
        }
    }
}

1.3 RedisCacheManager.java

package com.feng.shiro;

import com.feng.utils.RedisUtil;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * @ClassName: RedisCacheManager
 * @Description: 描述
 * @createTime: 2020/2/9 21:14
 * @Author: 冯凡利
 * @UpdateUser: 冯凡利
 * @Version: 0.0.1
 */
public class RedisCacheManager implements CacheManager {

    @Autowired
    private RedisUtil redisUtil;

    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
        return new RedisCache<>(s, redisUtil);
    }
}


1.4 jwt的三个类

我不贴了,不是核心代码,可直接看GitHub。
四个部分:

  1. JwtTokenUtil.java:jwt的工具类
  2. JwtPropertiesConfig.java:application.properties 中的jwt的属性读取类
  3. InitializerJwtPropertiesConfig.java:将 JwtPropertiesConfig.java进行注入。
  4. application.properties 中的jwt的自定义属性配置。

1.5 ShiroConfig.java新增Bean

新增了redisCacheManager bean,然后再 getShiroRealm() 函数中 ,通过 Realm(ShiroRealm) 进行设置 缓存。

    /**
     * shiro 的 缓存管理器
     * 需要在 ShiroRealm bean 中进行设置
     * @return
     */
    @Bean(name = "redisCacheManager")
    public RedisCacheManager redisCacheManager() {
        return new RedisCacheManager();
    }


    /**
     * 登录的 认证域
     *
     * @param hashedCredentialsMatcher
     * @return
     */
    @Bean(name = "shiroRealm")
    public ShiroRealm getShiroRealm(@Qualifier("shiroHashedCredentialsMatcher") HashedCredentialsMatcher hashedCredentialsMatcher,
                                    @Qualifier("redisCacheManager") RedisCacheManager redisCacheManager) {
        ShiroRealm shiroRealm = new ShiroRealm();
        // 自定义 处理 token 过滤
        shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher);
        // 自定义 缓存管理器
        shiroRealm.setCacheManager(redisCacheManager);
        return shiroRealm;
    }

shiro 新增的 shiro 缓存 代码到此结束。
其他代码没有展示,是因为 shiro 缓存的代码只有这些。

2. 源码讲解。

2.1 shiro 缓存的代码流程。

  1. 首先编写 RedisCache.java:实现 Cache 接口,再这里重写redis的一些操作函数
  2. 编写RedisCacheManager.java 类:实现 CacheManager 接口,将RedisCache.java 类加入到 缓存管理中。
  3. 编写ShiroConfig.java 类:将RedisCacheManager注入生Bean,然后再Realm(本案例是ShiroRealm)中设置 缓存管理,同时也是设置 资格匹配 的地方。

2.2 缓存流程

shiro的核心是再 数据域中,也就是Realm,本案例的是ShiroRealm,继承 AuthorizingRealm 授权域, 授权域 继承了 AuthenticatingRealm 认证域,认证域 又继承了 CachingRealm 缓存域。这三个类是shiro 认证、授权、缓存 三个最重要的类。

2.2.1 认证和授权简述

继承了 AuthorizingRealm 授权域,就要重写两个最重要的方法

  1. 认证函数 doGetAuthenticationInfo(),既然认证,重写的就是 AuthenticatingRealm 认证域 中的函数。在函数getAuthenticationInfo() 中。源码截图如下:
    在这里插入图片描述

  2. 授权函数 doGetAuthorizationInfo(),既然授权,重写的就是 AuthorizingRealm 授权域 中的函数。在函数 getAuthorizationInfo() 中,源码截图如下:
    在这里插入图片描述

在 Shiro 中,认证缓存(authentication cache) 和 授权缓存(authorization cache) 是两个不同的缓存,它们用于不同的目的:

  • 认证缓存 (authenticationCache):用于缓存用户的认证信息(如,用户是否存在,凭证是否正确等)。这通常是一个比较频繁更新的缓存,因为用户登录的凭证(如密码)是动态变化的,可能会变得无效。
  • 授权缓存 (authorizationCache):用于缓存用户的授权信息(如,用户的角色、权限等)。授权信息通常不太会频繁变动,因此可以缓存很长时间。它一般是用于存储用户的角色和权限信息,以减少每次请求时对数据库或外部系统的查询。

2.2.2 AuthenticatingRealm.getAuthenticationInfo() 认证缓存。

该函数的第一句就是:AuthenticationInfo info = this.getCachedAuthenticationInfo(token); 该函数就是从缓存中获取认证信息。
先说答案:shiro不推荐、默认不支持 认证从缓存中获取。

代码中可以看到 在AuthenticatingRealm类属性 this.authenticationCachingEnabled = false; 在构造函数中 默认为 false,不开启缓存。

究其原因:用于缓存用户的认证信息(如,用户是否存在,凭证是否正确等)。这通常是一个比较频繁更新的缓存,因为用户登录的凭证(如密码)是动态变化的,可能会变得无效。

所以不推荐缓存,通过debug,也可以看到。

2.2.3 AuthorizingRealm.getAuthorizationInfo() 授权缓存。

授权缓存 在shiro 中是支持的。因为 AuthorizingRealm 授权域类中的 类属性 this.authorizationCachingEnabled = true; 在构造函数中默认为true的。

getAuthorizationInfo() 函数就是从缓存中进行获取的,没有获取到在 进入 到 doGetAuthorizationInfo() 函数中进行获取、授权。

可以通过debug的方式一点点去查看。

如果没有设置redis作为缓存管理,默认可以配置缓存,但是shiro并没有帮我指定缓存,估计是怕默认指定之后怕内存使用过高,需要自行设定,接下来讲解。

2.3 redis缓存的设置入口(个人认为是重点)

前边讲的都是逻辑流程,如何从缓存中获取。我在读源码的时候我就很好奇,那设置redis作为缓存的地方在哪里呢?默认是用内存作为缓存的设置又在哪里呢?抱着这些疑问继续走。

答案:入口就在ShiroConfig中 Realm 的配置中。

     ShiroRealm shiroRealm = new ShiroRealm();
     // 自定义 处理 token 过滤
     shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher);
     // 自定义 缓存管理器
     shiroRealm.setCacheManager(redisCacheManager);

new ShiroRealm()时 ;会执行 AuthorizingRealm、AuthenticatingRealm的构造函数。构造函数中会设置 缓存启动情况、缓存的配置情况。

  • 其中 AuthenticatingRealm 认证域的构造函数中,会默认指定 SimpleCredentialsMatcher 作为 凭证匹配器 的 类,shiro还提供了 HashedCredentialsMatcher 作为 凭证匹配器 类(具体有几个,看 CredentialsMatcher 类的实现类有几个即可,下图所示)。后面我们自定义了 凭证匹配器 类 CustomHashedCredentialsMatcher,其中的 doCredentialsMatch() 函数就是默认 认证的最核心的函数。
    在这里插入图片描述

  • AuthorizingRealm 授权域的构造函数中,会默认指定 缓存管理(在CachingRealm 类中)、凭证匹配器(认证域类中) 的初始化,默认都是null。

  • shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher);:调用 认证域类 中的方法设置 凭证匹配器

  • shiroRealm.setCacheManager(redisCacheManager);:调用 缓存域类 中的方法设置缓存管理器 以及 实现缓存的方式(第二行语句),该方法是个接口,有两个实现方法:一个是 认证域,一个是授权域。往下走。在这里插入图片描述

  • 这里就是 授权域 中的 afterCacheManagerSet() 函数,

    • 第一个函数调用父类也就是认证域上的函数,会继续调用 this.getAvailableAuthenticationCache()。认证域前边说了,认证缓存shiro默认是关闭的,debug走一边流程就明白了;重点就是授权域了。
    • 在第二个函数上,先判断是否有缓存,为null,然后再看 授权域 是否支持 授权缓存(默认支持,前边说了);然后进入懒加载缓存(下面第二张图);到获取缓存的地方,缓存管理不为null,是redis的,接着从158行,获取缓存,158行往下debug,就是上面的1.3小节的 RedisCacheManager.java,将redis返回。
      在这里插入图片描述
      在这里插入图片描述
  • 我又来问题了,如果没有redis,走的是哪个呢?首先我也不知道,得先把redis缓存管理给去掉。看看使用的哪个。CacheManager.java 是接口,其实现类截图如下,第一个是我实现的,其余三个都是shiro自带的(第二个应该不是,看源码)。

  • 通过源码查看,shiro并没有默认使用缓存配置,仅仅是打开了缓存配置,可以供我们使用,如果想用使用缓存需要自行配置缓存,想上面配置 redis一样,将下面的第二个或者第四个配置到缓存管理中去。
    在这里插入图片描述

到这里为止源码讲解完啦。

3. 问题解决

3.1 授权的流程是怎么做的

这个需要有背景的。
我基于这个项目来说一下
前端 LayUI+thymeleaf
后端使用springboot、shiro作为权限管理。
根据请求 /api/users,进行举例。
前端发请求后,会先认证,此处略。
然后会先从前端标签获取 权限字符串 sys:user:list,这里使用到了 thymeleaf,然后 通过 doGetAuthorizationInfo()获取所有权限字符串和角色,然后与从前端拿到的 权限字符串 sys:user:list进行对比,若包含,这通过。否则失败

3.2 @RequiresPermissions(“sys:user:list”)是如何工作的

1. 基本原理

@RequiresPermissions("sys:user:list") 是 Apache Shiro 提供的权限控制注解,用于声明访问某个方法或类需要的特定权限。

它的工作机制如下:

  1. 拦截注解:

    • Shiro 提供了 AuthorizationAttributeSourceAdvisor 和 AOP(切面编程)机制来拦截被注解标记的类或方法。
    • 当调用被标记的方法时,Shiro 拦截并执行权限校验。
  2. 获取当前用户的权限:

    • Shiro 从当前登录用户的 Subject(主体)中获取用户的权限信息(通常是一组权限字符串)。
  3. 权限字符串匹配:

    • Shiro 将注解中的权限字符串(如 "sys:user:list")与当前用户的权限列表进行比对。
    • 如果用户的权限列表中包含注解指定的权限字符串,则校验通过;否则抛出 AuthorizationException,拒绝访问。

2. 具体流程

假设你有以下代码:

@RequiresPermissions("sys:user:list")
public void listUsers() {
    // 获取用户列表逻辑
}
1. 用户登录阶段
  • 用户通过登录接口认证成功后,Shiro 会根据用户身份(如用户名)从数据库或缓存中加载用户的所有权限,并存储在 Subject 中。
  • 示例权限列表:
    ["sys:user:list", "sys:user:edit", "sys:user:delete"]
    
2. 调用 listUsers 方法时
  • Shiro 拦截 @RequiresPermissions 注解,通过当前用户的 Subject 获取权限列表。
  • 调用 Subject.isPermitted("sys:user:list") 方法,具体逻辑如下:
    1. Shiro 将注解中的权限字符串 "sys:user:list" 传递给 AuthorizingRealmdoGetAuthorizationInfo 方法。
    2. doGetAuthorizationInfo 返回用户的权限列表。
    3. Shiro 比较 "sys:user:list" 是否存在于用户的权限列表中:
      • 存在:校验通过,继续执行方法。
      • 不存在:抛出异常 AuthorizationException
3. 匹配机制
  • 权限字符串可以使用通配符(Wildcard)进行匹配:
    • 完全匹配:"sys:user:list" 对比 "sys:user:list",通过。
    • 通配符匹配:"sys:user:*" 对比 "sys:user:list",通过。
    • 多级通配符:"sys:*:*" 对比 "sys:user:list",通过。

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

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

相关文章

属性编程与权限编程

问题 如何获取文件的大小&#xff0c;时间戳以及类型等信息&#xff1f; 再论 inode 文件的物理载体是硬盘&#xff0c;硬盘的最小存储单元是扇区 (每个扇区 512 字节) 文件系统以 块 为单位(每个块 8 个扇区) 管理文件数据 文件元信息 (创建者、创建日期、文件大小&#x…

用 HTML、CSS 和 JavaScript 实现抽奖转盘效果

顺序抽奖 前言 这段代码实现了一个简单的抽奖转盘效果。页面上有一个九宫格布局的抽奖区域&#xff0c;周围八个格子分别放置了不同的奖品名称&#xff0c;中间是一个 “开始抽奖” 的按钮。点击按钮后&#xff0c;抽奖区域的格子会快速滚动&#xff0c;颜色不断变化&#xf…

R语言绘制有向无环图(DAG)

有向无环图&#xff08;Directed Acyclic Graph&#xff0c;简称DAG&#xff09;是一种特殊的有向图&#xff0c;它由一系列顶点和有方向的边组成&#xff0c;其中不存在任何环路。这意味着从任一顶点出发&#xff0c;沿着箭头方向移动&#xff0c;你永远无法回到起始点。 从流…

Spring Web MVC基础第一篇

目录 1.什么是Spring Web MVC&#xff1f; 2.创建Spring Web MVC项目 3.注解使用 3.1RequestMapping&#xff08;路由映射&#xff09; 3.2一般参数传递 3.3RequestParam&#xff08;参数重命名&#xff09; 3.4RequestBody&#xff08;传递JSON数据&#xff09; 3.5Pa…

129.求根节点到叶节点数字之和(遍历思想)

Problem: 129.求根节点到叶节点数字之和 文章目录 题目描述思路复杂度Code 题目描述 思路 遍历思想(利用二叉树的先序遍历) 直接利用二叉树的先序遍历&#xff0c;将遍历过程中的节点值先利用字符串拼接起来遇到根节点时再转为数字并累加起来&#xff0c;在归的过程中&#xf…

unity中的动画混合树

为什么需要动画混合树&#xff0c;动画混合树有什么作用&#xff1f; 在Unity中&#xff0c;动画混合树&#xff08;Animation Blend Tree&#xff09;是一种用于管理和混合多个动画状态的工具&#xff0c;包括1D和2D两种类型&#xff0c;以下是其作用及使用必要性的介绍&…

MySQL存储过程和存储函数_mysql 存储过 call proc_stat_data(3,null)

2&#xff09;很难调试存储过程。只有少数数据库管理系统允许调试存储过程。不幸的是&#xff0c;MySQL不提供调试存储过程的功能。 1.2 数据准备 创建数据库&#xff1a; DEFAULT CHARACTER SET utf8; use test;这里记得设置编码&#xff01; 创建测试表&#xff1a; DROP…

Flink2支持提交StreamGraph到Flink集群

最近研究Flink源码的时候&#xff0c;发现Flink已经支持提交StreamGraph到集群了&#xff0c;替换掉了原来的提交JobGraph。 新增ExecutionPlan接口&#xff0c;将JobGraph和StreamGraph作为实现。 Flink集群Dispatcher也进行了修改&#xff0c;从JobGraph改成了接口Executio…

Vue 入门到实战 七

第7章 渲染函数 目录 7.1 DOM树 7.2 什么是渲染函数 7.3 h()函数 7.3.1 基本参数 7.3.2 约束 7.3.3 使用JavaScript代替模板功能 7.1 DOM树 7.2 什么是渲染函数 在多数情况下&#xff0c;Vue推荐使用模板template来创建HTML。然而在一些应用场景中&#xff0c;需要使用J…

系统学习算法: 专题八 二叉树中的深搜

深搜其实就是深度优先遍历&#xff08;dfs&#xff09;&#xff0c;与此相对的还有宽度优先遍历&#xff08;bfs&#xff09; 如果学完数据结构有点忘记&#xff0c;如下图&#xff0c;左边是dfs&#xff0c;右边是bfs 而二叉树的前序&#xff0c;中序&#xff0c;后序遍历都可…

进程、线程、内存和IO模型的概念详解

进程、线程、内存和IO模型的概念详解 1 进程与线程1.1 进程1.1.1 进程分类1.1.2 进程的状态和转换1.1.3 僵尸进程和孤儿进程的区别1.1.4 进程之间的通信1.1.5 用户态和内核态1.1.6 用户空间和内核空间 1.2 线程1.2.1 线程的状态和转换1.2.2 进程与线程的区别 1.3 多进程和多线程…

Labelme转Voc、Coco

Q&#xff1a;在github找的cv代码基本都是根据现有且流行的公共数据集格式组织的训练数据集&#xff0c;这导致我使用labelme标注好之后需要我们重新组织数据集 labelme2coco #!/usr/bin/env pythonimport argparse import collections import datetime import glob import j…

JVM方法区

一、栈、堆、方法区的交互关系 二、方法区的理解: 尽管所有的方法区在逻辑上属于堆的一部分&#xff0c;但是一些简单的实现可能不会去进行垃圾收集或者进行压缩&#xff0c;方法区可以看作是一块独立于Java堆的内存空间。 方法区(Method Area)与Java堆一样&#xff0c;是各个…

【Python】第七弹---Python基础进阶:深入字典操作与文件处理技巧

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】【Linux系统编程】【MySQL】【Python】 目录 1、字典 1.1、字典是什么 1.2、创建字典 1.3、查找 key 1.4、新增/修改元素 1.5、删除元素 1.6、遍历…

在实际开发中,如何正确使用 INT(1) 和 INT(10)

在实际开发中&#xff0c;如何正确使用 INT(1) 和 INT(10) 前言 在数据库设计和开发过程中&#xff0c;数据类型的选择至关重要。 最近&#xff0c;我在工作中遇到了一个关于MySQL中INT类型的误解问题&#xff0c;这让我意识到很多开发者对INT类型的理解存在误区。 本文将深…

像接口契约文档 这种工件,在需求 分析 设计 工作流里面 属于哪一个工作流

οゞ浪漫心情ゞο(20***328) 2016/2/18 10:26:47 请教一下&#xff0c;像接口契约文档 这种工件&#xff0c;在需求 分析 设计 工作流里面 属于哪一个工作流&#xff1f; 潘加宇(35***47) 17:17:28 你这相当于问用例图、序列图属于哪个工作流&#xff0c;看内容。 如果你的&quo…

GAMES101学习笔记(六):Geometry 几何(基本表示方法、曲线与曲面、网格处理)

文章目录 几何的表示方法隐式几何 Implicit Geometry代数曲面(Algebraic surface)构造实体几何CSG(Constructive Solid Geometry)距离函数(Distance Function)水平集方法(Level Set Methods)分型几何(Fractal) 显式几何 Explicit Geometry点云(Point Cloud)多边形网格(Polygon …

【Numpy核心编程攻略:Python数据处理、分析详解与科学计算】1.24 随机宇宙:生成现实世界数据的艺术

1.24 随机宇宙&#xff1a;生成现实世界数据的艺术 目录 #mermaid-svg-vN1An9qZ6t4JUcGa {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-vN1An9qZ6t4JUcGa .error-icon{fill:#552222;}#mermaid-svg-vN1An9qZ6t4JUc…

爬虫基础(三)Session和Cookie讲解

目录 一、前备知识点 &#xff08;1&#xff09;静态网页 &#xff08;2&#xff09;动态网页 &#xff08;3&#xff09;无状态HTTP 二、Session和Cookie 三、Session 四、Cookie &#xff08;1&#xff09;维持过程 &#xff08;2&#xff09;结构 正式开始说 Sessi…

HTMLCSS :下雪了

这段代码创建了一个动态的雪花飘落加载动画&#xff0c;通过 CSS 技术实现了雪花的下落和消失效果&#xff0c;为页面添加了视觉吸引力和动态感。 大家复制代码时&#xff0c;可能会因格式转换出现错乱&#xff0c;导致样式失效。建议先少量复制代码进行测试&#xff0c;若未能…