若依缓存使用浅析

2023-11-12

配置

这块主要涉及两个类

  • FastJson2JsonRedisSerializer : 继承 RedisSerializer 接口自定义使用 fastjson 进行序列化和反序列化
  • RedisConfig:配置使用 StringRedisSerializer 来进行key的序列化与反序列,使用刚才我们 FastJson2JsonRedisSerializer 来进行 value 的序列化与反序列

下面贴下相关代码

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{
    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        serializer.setObjectMapper(mapper);

        template.setValueSerializer(serializer);
        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{
    @SuppressWarnings("unused")
    private ObjectMapper objectMapper = new ObjectMapper();

    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private Class<T> clazz;

    static
    {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    public FastJson2JsonRedisSerializer(Class<T> clazz)
    {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException
    {
        if (t == null)
        {
            return new byte[0];
        }
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException
    {
        if (bytes == null || bytes.length <= 0)
        {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);

        return JSON.parseObject(str, clazz);
    }

    public void setObjectMapper(ObjectMapper objectMapper)
    {
        Assert.notNull(objectMapper, "'objectMapper' must not be null");
        this.objectMapper = objectMapper;
    }

    protected JavaType getJavaType(Class<?> clazz)
    {
        return TypeFactory.defaultInstance().constructType(clazz);
    }
}

简单提一嘴 ruoyi 的模块拆分,common 与 framework 的拆分思想很值得学习,之前我自己拆分的时候没有拆出 framework 这一层,就导致很多与具体业务模块无关的内容与业务模块代码耦合过于严重

这两个配置类放在了 framework 模块中,然后所有对 redistemplate 的操作封装在了 common 的工具类 RedisCache

应用场景浅析

我们通过全局搜索 RedisCache,可以很方便的找到 redis 在项目中的使用,下面我们一个一个走一遍

image-20221215120602216

登录验证码与登录token存储鉴权

这几个比较类型放在一起说

验证码这块,在获取验证码的时候会把 生成的验证码放到 redis 缓存,并设置 过期时间

redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);

简单提一下验证码工具,调用验证码工具类会生成一个图片和一个对应的code,我们在生成个uuid标识这次的结果,和短信登录意思差不多,只不过把手机号变成了 uuid,后面校验登录时传来的 uuid 和 code 是否匹配

后面看login这块,就是 token 存储的使用了

/**
     * 登录验证
     * 
     * @param username 用户名
     * @param password 密码
     * @param code 验证码
     * @param uuid 唯一标识
     * @return 结果
     */
    public String login(String username, String password, String code, String uuid)
    {
      // <1> 根据 uuid 和 code 去 redis 中找,并且删除掉这个验证码的code
        String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
        String captcha = redisCache.getCacheObject(verifyKey);
        redisCache.deleteObject(verifyKey);
        if (captcha == null)
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
            throw new CaptchaExpireException();
        }
        if (!code.equalsIgnoreCase(captcha))
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
            throw new CaptchaException();
        }
        // 用户验证
        Authentication authentication = null;
        try
        {
            //<2> 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
        }
        catch (Exception e)
        {
            if (e instanceof BadCredentialsException)
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
                throw new UserPasswordNotMatchException();
            }
            else
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
                throw new CustomException(e.getMessage());
            }
        }
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
      // <3> 拿到刚才的 LoginUser(UserDetail实现类)
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        // <4> 生成token
        return tokenService.createToken(loginUser);
    }

<1> 处看下注释即可

<2> 这块调用 UserDetailsServiceImpl.loadUserByUsername,调用自定义的函数,判断账号密码是否匹配,匹配后返回 UserDetails 的自定义实现类,这步还可以一起返回该用户所有权限

<3> <4> 调用 createToken方法在 redis 中记录 token(基于用户信息用 jwt 生成,但是也是基于持久化存储的,这种实现是最灵活且安全的)

springsecurity 不清楚的可以看这篇文章 芋道 Spring Boot 安全框架 Spring Security 入门 | 芋道源码 —— 纯源码解析博客 (iocoder.cn)

接着看后面这块实现 TokenService

/**
     * 创建令牌
     *
     * @param loginUser 用户信息
     * @return 令牌
     */
    public String createToken(LoginUser loginUser)
    {
      // <1> 用 uuid 作为key,这个token只是叫 token ,但其实是 key
        String token = IdUtils.fastUUID();
        loginUser.setToken(token);
        setUserAgent(loginUser);
      // <2> 把loginUser缓存到 redis
        refreshToken(loginUser);

      // <3> 生成 jwt 的 token 
        Map<String, Object> claims = new HashMap<>();
        claims.put(Constants.LOGIN_USER_KEY, token);
        return createToken(claims);
    }

/**
     * 刷新令牌有效期
     *
     * @param loginUser 登录信息
     */
    public void refreshToken(LoginUser loginUser)
    {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        String userKey = getTokenKey(loginUser.getToken());
        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }

/**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private String createToken(Map<String, Object> claims)
    {
        String token = Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret).compact();
        return token;
    }

image-20221215173219187

查看redis 可以看到我们序列化的信息

image-20221215173338131

debug下,看到我们把生成的uuid再使用jwt加密返回,然后 uuid 关联用户信息的json序列化后存储到 redis

image-20221215173437579

还是要debug啊

然后把生成的token返回给前端,后面的请求前端会带上这个 token

下面来讲下token是如何被解析的,也就是debug需要校验是否登录的接口,这块是JwtAuthenticationTokenFilter中实现的

@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException
    {
      // <1>
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
        {
          // <2>
            tokenService.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        chain.doFilter(request, response);
    }

// tokenService
 /**
     * 获取用户身份信息
     *
     * @return 用户信息
     */
    public LoginUser getLoginUser(HttpServletRequest request)
    {
        // 获取请求携带的令牌
        String token = getToken(request);
        if (StringUtils.isNotEmpty(token))
        {
            Claims claims = parseToken(token);
            // 解析对应的权限以及用户信息
            String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
            String userKey = getTokenKey(uuid);
            LoginUser user = redisCache.getCacheObject(userKey);
            return user;
        }
        return null;
    }

/**
     * 验证令牌有效期,相差不足20分钟,自动刷新缓存
     *
     * @param loginUser
     * @return 令牌
     */
    public void verifyToken(LoginUser loginUser)
    {
        long expireTime = loginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
        {
            refreshToken(loginUser);
        }
    }

<1> 从 header 中取出 token,用 jwt 解密出来 uuid,看下这个uuid是否在redis中存在

<2> 判断 token 时效性,刷新 token

在线用户统计与签退

下面来看在线用户的实现

@PreAuthorize("@ss.hasPermi('monitor:online:list')")
    @GetMapping("/list")
    public TableDataInfo list(String ipaddr, String userName)
    {
        Collection<String> keys = redisCache.keys(Constants.LOGIN_TOKEN_KEY + "*");
        List<SysUserOnline> userOnlineList = new ArrayList<SysUserOnline>();
        for (String key : keys)
        {
            LoginUser user = redisCache.getCacheObject(key);
            if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName))
            {
                if (StringUtils.equals(ipaddr, user.getIpaddr()) && StringUtils.equals(userName, user.getUsername()))
                {
                    userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user));
                }
            }
            else if (StringUtils.isNotEmpty(ipaddr))
            {
                if (StringUtils.equals(ipaddr, user.getIpaddr()))
                {
                    userOnlineList.add(userOnlineService.selectOnlineByIpaddr(ipaddr, user));
                }
            }
            else if (StringUtils.isNotEmpty(userName) && StringUtils.isNotNull(user.getUser()))
            {
                if (StringUtils.equals(userName, user.getUsername()))
                {
                    userOnlineList.add(userOnlineService.selectOnlineByUserName(userName, user));
                }
            }
            else
            {
                userOnlineList.add(userOnlineService.loginUserToUserOnline(user));
            }
        }
        Collections.reverse(userOnlineList);
        userOnlineList.removeAll(Collections.singleton(null));
        return getDataTable(userOnlineList);
    }

就是拿到所有在线用户的key,模糊查询,然后取set

强退用户

/**
     * 强退用户
     */
    @PreAuthorize("@ss.hasPermi('monitor:online:forceLogout')")
    @Log(title = "在线用户", businessType = BusinessType.FORCE)
    @DeleteMapping("/{tokenId}")
    public AjaxResult forceLogout(@PathVariable String tokenId)
    {
        redisCache.deleteObject(Constants.LOGIN_TOKEN_KEY + tokenId);
        return AjaxResult.success();
    }

在 redis 里面把这条登录的记录删除掉

系统设置的缓存

这块在用的时候最直观的就是项目启动时会发现有很多查询sys_config的sql的debug信息,其中第一条就是加载系统配置啦,下面的就是加载数据字典,放在后面说

image-20221215174919761

/**
     * 项目启动时,初始化参数到缓存
     */
    @PostConstruct
    public void init()
    {
        List<SysConfig> configsList = configMapper.selectConfigList(new SysConfig());
        for (SysConfig config : configsList)
        {
            redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue());
        }
    }

SysConfigServiceImpl 中配置了这个,会在项目启动时初始化,拿到所有 sys_config,加载到redis

image-20221215175010582

看下查询配置的实现

/**
     * 根据键名查询参数配置信息
     * 
     * @param configKey 参数key
     * @return 参数键值
     */
    @Override
    public String selectConfigByKey(String configKey)
    {
      // <1> 首先从缓存中取,存在返回缓存中的
        String configValue = Convert.toStr(redisCache.getCacheObject(getCacheKey(configKey)));
        if (StringUtils.isNotEmpty(configValue))
        {
            return configValue;
        }
      // <2> 不存在的话,查数据库,数据库中存在,写缓存并返回
        SysConfig config = new SysConfig();
        config.setConfigKey(configKey);
        SysConfig retConfig = configMapper.selectConfig(config);
        if (StringUtils.isNotNull(retConfig))
        {
            redisCache.setCacheObject(getCacheKey(configKey), retConfig.getConfigValue());
            return retConfig.getConfigValue();
        }
        return StringUtils.EMPTY;
    }

新增,修改与删除,看下是否存在缓存与db不一致的情况

/**
     * 新增参数配置
     * 
     * @param config 参数配置信息
     * @return 结果
     */
    @Override
    public int insertConfig(SysConfig config)
    {
        int row = configMapper.insertConfig(config);
        if (row > 0)
        {
            redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue());
        }
        return row;
    }

    /**
     * 修改参数配置
     * 
     * @param config 参数配置信息
     * @return 结果
     */
    @Override
    public int updateConfig(SysConfig config)
    {
        int row = configMapper.updateConfig(config);
        if (row > 0)
        {
            redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue());
        }
        return row;
    }

    /**
     * 批量删除参数信息
     * 
     * @param configIds 需要删除的参数ID
     * @return 结果
     */
    @Override
    public int deleteConfigByIds(Long[] configIds)
    {
        for (Long configId : configIds)
        {
            SysConfig config = selectConfigById(configId);
            if (StringUtils.equals(UserConstants.YES, config.getConfigType()))
            {
                throw new CustomException(String.format("内置参数【%1$s】不能删除 ", config.getConfigKey()));
            }
        }
        int count = configMapper.deleteConfigByIds(configIds);
        if (count > 0)
        {
            Collection<String> keys = redisCache.keys(Constants.SYS_CONFIG_KEY + "*");
            redisCache.deleteObject(keys);
        }
        return count;
    }

新增与修改,删除,先写数据库再写redis,如果写表成功,写redis,都是基于这个原则,可以思考下为什么不能先写 redis(缓存不一致问题)

数据字典

数据字典的键和值是分开存储的

image-20221215180312694

image-20221215180336103

根据 dict_code 关联存储

还是看下初始化代码

/**
     * 项目启动时,初始化字典到缓存
     */
    @PostConstruct
    public void init()
    {
        List<SysDictType> dictTypeList = dictTypeMapper.selectDictTypeAll();
        for (SysDictType dictType : dictTypeList)
        {
            List<SysDictData> dictDatas = dictDataMapper.selectDictDataByType(dictType.getDictType());
            DictUtils.setDictCache(dictType.getDictType(), dictDatas);
        }
    }

// DictUtils
/**
     * 设置字典缓存
     * 
     * @param key 参数键
     * @param dictDatas 字典数据列表
     */
    public static void setDictCache(String key, List<SysDictData> dictDatas)
    {
        SpringUtils.getBean(RedisCache.class).setCacheObject(getCacheKey(key), dictDatas);
    }

先查主表,然后关联查询子表

子表的数据集合整个序列化成一个json,举一个dict为例看下存储在 redis 里面的 value(加 @ 是防止关键字冲突?)

image-20221215180810604
[{"@type":"com.ruoyi.common.core.domain.entity.SysDictData","createBy":"admin","createTime":1658803658000,"default":false,"dictCode":147,"dictLabel":"工业","dictSort":1,"dictType":"attributes","dictValue":"1","isDefault":"N","params":{"@type":"java.util.HashMap"},"status":"0"},{"@type":"com.ruoyi.common.core.domain.entity.SysDictData","createBy":"admin","createTime":1658803659000,"default":false,"dictCode":148,"dictLabel":"商业","dictSort":2,"dictType":"attributes","dictValue":"2","isDefault":"N","params":{"@type":"java.util.HashMap"},"status":"0"}]

后面的分析比较类似,大家有兴趣自己 debug 把(其实是因为懒,哈哈)

后记

其实写这个系列完全是兴趣,因为 redis 的一些东西看了好多,但是不知道具体是怎么用的, 索性就拿若依来看看吧

后面想到啥了再补充把,先写到这里

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

若依缓存使用浅析 的相关文章

  • java.lang.NoClassDefFoundError:org.apache.batik.dom.svg.SVGDOMImplementation

    我在链接到我的 Android LibGDX 项目的 Apache Batik 库时遇到了奇怪的问题 但让我们从头开始 在 IntelliJ Idea 中我有一个项目 其中包含三个模块 Main Android 和 Desktop 我强调的
  • Java new Date() 打印

    刚刚学习 Java 我知道这可能听起来很愚蠢 但我不得不问 System out print new Date 我知道参数中的任何内容都会转换为字符串 最终值是 new Date 返回对 Date 对象的引用 那么它是如何打印这个的呢 Mo
  • 如何默认将 Maven 插件附加到阶段?

    我有一个 Maven 插件应该在编译阶段运行 所以在项目中consumes我的插件 我必须做这样的事情
  • 为什么 i++ 不是原子的?

    Why is i Java 中不是原子的 为了更深入地了解 Java 我尝试计算线程中循环的执行频率 所以我用了一个 private static int total 0 在主课中 我有两个线程 主题 1 打印System out prin
  • 如何在 Play java 中创建数据库线程池并使用该池进行数据库查询

    我目前正在使用 play java 并使用默认线程池进行数据库查询 但了解使用数据库线程池进行数据库查询可以使我的系统更加高效 目前我的代码是 import play libs Akka import scala concurrent Ex
  • 给定两个 SSH2 密钥,我如何检查它们是否属于 Java 中的同一密钥对?

    我正在尝试找到一种方法来验证两个 SSH2 密钥 一个私有密钥和一个公共密钥 是否属于同一密钥对 我用过JSch http www jcraft com jsch 用于加载和解析私钥 更新 可以显示如何从私钥 SSH2 RSA 重新生成公钥
  • INSERT..RETURNING 在 JOOQ 中不起作用

    我有一个 MariaDB 数据库 我正在尝试在表中插入一行users 它有一个生成的id我想在插入后得到它 我见过this http www jooq org doc 3 8 manual sql building sql statemen
  • JavaMail 只获取新邮件

    我想知道是否有一种方法可以在javamail中只获取新消息 例如 在初始加载时 获取收件箱中的所有消息并存储它们 然后 每当应用程序再次加载时 仅获取新消息 而不是再次重新加载它们 javamail 可以做到这一点吗 它是如何工作的 一些背
  • 我可以使用 HSQLDB 进行 junit 测试克隆 mySQL 数据库吗

    我正在开发一个 spring webflow 项目 我想我可以使用 HSQLDB 而不是 mysql 进行 junit 测试吗 如何将我的 mysql 数据库克隆到 HSQLDB 如果您使用 spring 3 1 或更高版本 您可以使用 s
  • JRE 系统库 [WebSphere v6.1 JRE](未绑定)

    将项目导入 Eclipse 后 我的构建路径中出现以下错误 JRE System Library WebSphere v6 1 JRE unbound 谁知道怎么修它 右键单击项目 特性 gt Java 构建路径 gt 图书馆 gt JRE
  • 使用Caliper时如何指定命令行?

    我发现 Google 的微型基准测试项目 Caliper 非常有趣 但文档仍然 除了一些示例 完全不存在 我有两种不同的情况 需要影响 JVM Caliper 启动的命令行 我需要设置一些固定 最好在几个固定值之间交替 D 参数 我需要指定
  • Java Integer CompareTo() - 为什么使用比较与减法?

    我发现java lang Integer实施compareTo方法如下 public int compareTo Integer anotherInteger int thisVal this value int anotherVal an
  • Eclipse Java 远程调试器通过 VPN 速度极慢

    我有时被迫离开办公室工作 这意味着我需要通过 VPN 进入我的实验室 我注意到在这种情况下使用 Eclipse 进行远程调试速度非常慢 速度慢到调试器需要 5 7 分钟才能连接到远程 jvm 连接后 每次单步执行断点 行可能需要 20 30
  • 仅将 char[] 的一部分复制到 String 中

    我有一个数组 char ch 我的问题如下 如何将 ch 2 到 ch 7 的值合并到字符串中 我想在不循环 char 数组的情况下实现这一点 有什么建议么 感谢您花时间回答我的问题 Use new String value offset
  • 如何在桌面浏览器上使用 webdriver 移动网络

    我正在使用 selenium webdriver 进行 AUT 被测应用程序 的功能测试自动化 AUT 是响应式网络 我几乎完成了桌面浏览器的不同测试用例 现在 相同的测试用例也适用于移动浏览器 因为可以从移动浏览器访问 AUT 由于它是响
  • 获取 JVM 上所有引导类的列表?

    有一种方法叫做findBootstrapClass对于一个类加载器 如果它是引导的 则返回一个类 有没有办法找到类已经加载了 您可以尝试首先通过例如获取引导类加载器呼叫 ClassLoader bootstrapLoader ClassLo
  • 静态变量的线程安全

    class ABC implements Runnable private static int a private static int b public void run 我有一个如上所述的 Java 类 我有这个类的多个线程 在里面r
  • JGit 检查分支是否已签出

    我正在使用 JGit 开发一个项目 我设法删除了一个分支 但我还想检查该分支是否已签出 我发现了一个变量CheckoutCommand但它是私有的 private boolean isCheckoutIndex return startCo
  • java.lang.IllegalStateException:驱动程序可执行文件的路径必须由 webdriver.chrome.driver 系统属性设置 - Similiar 不回答

    尝试学习 Selenium 我打开了类似的问题 但似乎没有任何帮助 我的代码 package seleniumPractice import org openqa selenium WebDriver import org openqa s
  • 将 List 转换为 JSON

    Hi guys 有人可以帮助我 如何将我的 HQL 查询结果转换为带有对象列表的 JSON 并通过休息服务获取它 这是我的服务方法 它返回查询结果列表 Override public List

随机推荐

  • 面试总结-2023届安全面试题总汇

    2023届安全面试题总汇 文章目录 2023届安全面试题总汇 前言 0x01 秋招目录 随时更新 0x02 各大安全厂商面试题 资料链接 一个2023届毕业生在毕业前持续更新 收集的安全岗面试题及面试经验分享 前言 最近发现一个宝贵的面试文
  • vector的find用法

    一 find函数存在于算法中 其头文件为 include
  • 驱动 - platform总线驱动

    include
  • 怎么求解100个正整数的最大公约数python

    答 你可以使用Python中的fractions模块来求解100个正整数的最大公约数 你需要先导入它 import fractions 然后你可以使用fractions gcd函数来求解 fractions gcd 100 200
  • Codeforces#808(Div.2)A-D题解

    目录 A Difference Operations B Difference of GCDs C Doremy s IQ D Difference Array A Difference Operations Problem A Codef
  • 2019年7款3D扫描仪APP(Android和iOS),让你手机秒变3D扫描仪!

    在我之前的一篇文章 教程 SolidWorks与3D扫描技术不得不说的故事 中 提到了SolidWorks和3D扫描技术之间的完美合作 今天就继续围绕3D扫描话题 为大家分享7款2019年3D扫描仪APP Android和iOS 喜欢就继续
  • Linux 解决sudo后接命令,仍旧权限不足的问题

    将本想执行的 sudo echo aa gt gt root text txt 改为 sudo sh c echo aa gt gt root text txt
  • Docker 1.9的新网络特性,以及Overlay详解

    本文转载自灵雀云技术博客 原文链接 http www alauda cn 2016 01 18 docker 1 9 network 作者简介 林帆 ThoughtWorks公司软件工程师及DevOps咨询师 具有丰富的持续交付和服务器运维
  • CCF 2104年3月第一题--相反数(java)

    代码如下 package com hsx ccf import java util Scanner public class Ccf20140301 public static void main String args SuppressW
  • Spring Cloud中的Hystrix的实现和使用

    Spring Cloud Hystrix 是 Spring Cloud 生态系统中的一个断路器组件 它可以帮助开发者优雅地处理分布式系统中的故障 提高系统的容错能力 下面介绍 Spring Cloud Hystrix 的实现和使用 引入依赖
  • QT获取lineEdit内容并写入文件中

    在ui中创建一个lineEdit 然后 QString sss ui gt lineEdit gt text 这样就获得了lineEdit的内容 并转为了QString格式 接下来参考 https editor csdn net md ar
  • 残差网络模型

    1 原始残差网络 最基本的残差块 中间的两层神经网络学习输入输出之间的残差 而旁边的链接就像一个高速公路 使得反向传播算法中的残差能通过这条路传到前边去 当网络变深时可以使得中间的输出为0 那么网络就能自适应的变成一个浅一点的网络 左边ba
  • Java 根据EXCEL下标获取EXCEL的列名

    通过根据EXCEL下标获取EXCEL的列名 用于给单元格设置公式用 num 是以0开头的下标 public static String getExcelColumn Integer num if num null return null S
  • 树的概念:层次、高度、深度、宽度

    目录 层次 宽度 深度 高度 其中只有层次是树原生的概念 其他都是从树中的结点来的 层次 从根节点开始算起 根节点算第一层 如图所示的树 第1层 A 第2层 B C 第3层 D E F 第4层 G H I 宽度 其实就是度 把结点的子树的棵
  • 大并发下请求合并(并发处理技巧)

    大并发下请求合并 一次请求消耗的资源 旧的方式 改造后 批量请求处理器 批量请求包装类 使用 性能测试 旧的 改造后的 一次请求消耗的资源 我们经常碰到查询请求的操作 例如根据用户id查询该用户的信息 接口仓储层查询用户正常的做法是通过id
  • adam算法介绍和总结

    19 adam算法 Adam 是一种可以替代传统随机梯度下降 SGD 过程的一阶优化算法 它能基于训练数据迭代地更新神经网络权重 Adam 最开始是由 OpenAI 的 Diederik Kingma 和多伦多大学的 Jimmy Ba 在提
  • ubuntu18.04编译Openwrt出现的问题解决

    ubuntu18 04编译Openwrt出现的问题解决 问题1 Build dependency Please install Git git core gt 1 6 5 问题2 gdate c 2497 7 error format no
  • 微信小程序授权打开摄像头,授权相册保存图片

    1 授权打开摄像头 doTakePhoto let that this wx getSetting success res 第一次未授权 if res authSetting scope camera undefined wx author
  • vscode中配置代码片段

    步骤如下 设置 gt 用户代码片段 新增全局代码片段 起全局代码片段文件名 xxx code snippets 这里以配置vue2初始代码片段为例 配置具体代码片段 按enter进入文件配置 以下是vue2的代码片段示例 具体可以自己随意配
  • 若依缓存使用浅析

    配置 这块主要涉及两个类 FastJson2JsonRedisSerializer 继承 RedisSerializer 接口自定义使用 fastjson 进行序列化和反序列化 RedisConfig 配置使用 StringRedisSer