Redis实战-详细配置-优雅的使用Redis注解/RedisTemplate

1. 简介

当我们对redis的基本知识有一定的了解后,我们再通过实战的角度学习一下在SpringBoot环境下,如何优雅的使用redis。

我们通过使用SpringBoot内置的Redis注解(文章最后有解释)来操作User相关的信息,

再通过Redis工具类的方式操作Role相关信息来全面的学习Redis的使用。

嫌篇幅太长的 可以直接跳到2.6查看具体逻辑即可。

2. 开撸

2.1 项目结构

结构说明:

├── src │   └── main │   ├── java │   │   └── com │   │   └── ldx │   │   └── redis │   │   ├── RedisApplication.java # 启动类 │   │   ├── config │   │   │   └── RedisConfig.java # redis 配置类 │   │   ├── constant │   │   │   └── CacheConstant.java # 缓存key常量类 │   │   ├── controller │   │   │   ├── RoleController.java # 角色管理控制器 │   │   │   └── UserController.java # 用户管理控制器 │   │   ├── entity │   │   │   ├── SysRole.java # 角色entity │   │   │   └── SysUser.java # 用户entity │   │   ├── mapper │   │   │   ├── SysRoleMapper.java # 角色持久层 │   │   │   └── SysUserMapper.java # 用户持久层 │   │   ├── service │   │   │   ├── SysRoleService.java # 角色接口层 │   │   │   ├── SysUserService.java # 用户接口层 │   │   │   └── impl │   │   │   ├── SysRoleServiceImpl.java # 角色接口实现层 │   │   │   └── SysUserServiceImpl.java # 用户接口实现层 │   │   └── util │   │   └── RedisUtil.java # redis 工具类 │   └── resources │   └── application.yaml # 系统配置文件 └── pom.xml # 依赖管理

2.2 导入依赖

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.3</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.ldx</groupId> <artifactId>redis</artifactId> <version>0.0.1-SNAPSHOT</version> <name>redis</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!--spring-web--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- lettuce pool --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!-- mybatis-plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.2</version> </dependency> <!-- mysql驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- lombok 工具包 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>

2.3 项目基本配置

2.3.1 application.yaml

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: 123456 type: com.zaxxer.hikari.HikariDataSource # redis 配置 redis: # 地址 host: localhost # 端口,默认为6379 port: 6379 # 密码 password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池中的最小空闲连接 min-idle: 0 # 连接池中的最大空闲连接 max-idle: 8 # 连接池的最大数据库连接数 max-active: 8 # #连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms mybatis-plus: # 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置 mapper-locations: classpath*:mapper/*.xml # 设置别名包扫描路径,通过该属性可以给包中的类注册别名 type-aliases-package: com.ldx.redis.entity configuration: # 控制台sql打印 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 日志配置 logging: level: com.ldx.redis.service.impl: debug org.springframework: warn

2.3.2 启动类

@EnableCaching:激活缓存支持

@MapperScan: 扫描mapper接口层

import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; /** * 启动类 * @author ludangxin * @date 2021/8/11 */ @EnableCaching @MapperScan(basePackages = "com.ldx.redis.mapper") @SpringBootApplication public class RedisApplication { public static void main(String[] args) { SpringApplication.run(RedisApplication.class, args); } }

2.4 redis配置

2.4.1 RedisConfig

我们除了在application.yaml中加入redis的基本配置外,一般还需要配置redis key和value的序列化方式,如下:

注解:

  1. 其默认的序列化方式为JdkSerializationRedisSerializer,这种方式跨语言和可读性都不太好,我们将其切换为Jackson2JsonRedisSerializer

  2. 可以使用entryTtl()为对应的模块设置过期时长。

redisTemplate:参考redisTemplate()

import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import com.ldx.redis.constant.CacheConstant; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.cache.RedisCacheWriter; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.time.Duration; import java.util.HashMap; import java.util.Map; /** * redis配置类 * @author ludangxin * @date 2021/8/11 */ @Configuration public class RedisConfig { @Bean public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { //设置不同cacheName的过期时间 Map<String, RedisCacheConfiguration> configurations = new HashMap<>(16); // 序列化方式 Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = getJsonRedisSerializer(); RedisSerializationContext.SerializationPair<Object> serializationPair = RedisSerializationContext.SerializationPair.fromSerializer(jsonRedisSerializer); // 默认的缓存时间 Duration defaultTtl = Duration.ofSeconds(20L); // 用户模块的缓存时间 Duration userTtl = Duration.ofSeconds(50L); // 默认的缓存配置 RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() //.entryTtl(defaultTtl) .serializeValuesWith(serializationPair); // 自定义用户模块的缓存配置 自定义的配置可以覆盖默认配置(当前的模块) configurations.put(CacheConstant.USER_CACHE_NAME, RedisCacheConfiguration.defaultCacheConfig() //.entryTtl(userTtl) .serializeValuesWith(serializationPair) ); return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory)) .cacheDefaults(redisCacheConfiguration) .withInitialCacheConfigurations(configurations) // 事物支持 .transactionAware() .build(); } @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = getJsonRedisSerializer(); StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // key采用String的序列化方式 template.setKeySerializer(stringRedisSerializer); // hash的key也采用String的序列化方式 template.setHashKeySerializer(stringRedisSerializer); // value序列化方式采用jackson template.setValueSerializer(jsonRedisSerializer); // hash的value序列化方式采用jackson template.setHashValueSerializer(jsonRedisSerializer); // 支持事物 //template.setEnableTransactionSupport(true); template.afterPropertiesSet(); return template; } /** * 设置jackson的序列化方式 */ private Jackson2JsonRedisSerializer<Object> getJsonRedisSerializer() { Jackson2JsonRedisSerializer<Object> redisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); redisSerializer.setObjectMapper(om); return redisSerializer; } }

2.4.1 CacheConstant

我们为了防止redis中key的重复,尽量会给不同的数据主体加上不同的前缀,这样我们在查看和统计的时候也方便操作。

/** * 缓存key 常量类 * @author ludangxin * @date 2021/8/11 */ public interface CacheConstant { /** * 用户cache name */ String USER_CACHE_NAME = "user_cache"; /** * 用户信息缓存key前缀 */ String USER_CACHE_KEY_PREFIX = "user_"; /** * 角色cache name */ String ROLE_CACHE_NAME = "role_cache"; /** * 角色信息缓存key前缀 */ String ROLE_CACHE_KEY_PREFIX = "role_"; /** * 获取角色cache key * @param suffix 后缀 * @return key */ static String getRoleCacheKey(String suffix) { return ROLE_CACHE_NAME + "::" + ROLE_CACHE_KEY_PREFIX + suffix; } }

2.4.2 RedisUtil

import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.*; import org.springframework.stereotype.Component; import java.util.*; import java.util.concurrent.TimeUnit; /** * spring redis 工具类 * @author ludangxin **/ @Component @RequiredArgsConstructor @SuppressWarnings(value = { "unchecked", "rawtypes" }) public class RedisUtil { public final RedisTemplate redisTemplate; /** * 缓存基本的对象,Integer、String、实体类等 * @param key 缓存的键值 * @param value 缓存的值 * @return 缓存的对象 */ public <T> ValueOperations<String, T> setCacheObject(String key, T value) { ValueOperations<String, T> operation = redisTemplate.opsForValue(); operation.set(key, value); return operation; } /** * 缓存基本的对象,Integer、String、实体类等 * @param key 缓存的键值 * @param value 缓存的值 * @param timeout 时间 * @param timeUnit 时间颗粒度 * @return 缓存的对象 */ public <T> ValueOperations<String, T> setCacheObject(String key, T value, Integer timeout, TimeUnit timeUnit) { ValueOperations<String, T> operation = redisTemplate.opsForValue(); operation.set(key, value, timeout, timeUnit); return operation; } /** * 获得缓存的基本对象。 * @param key 缓存键值 * @return 缓存键值对应的数据 */ public <T> T getCacheObject(String key) { ValueOperations<String, T> operation = redisTemplate.opsForValue(); return operation.get(key); } /** * 删除单个对象 * @param key */ public void deleteObject(String key) { redisTemplate.delete(key); } /** * 删除集合对象 * @param collection */ public void deleteObject(Collection collection) { redisTemplate.delete(collection); } /** * 缓存List数据 * @param key 缓存的键值 * @param dataList 待缓存的List数据 * @return 缓存的对象 */ public <T> ListOperations<String, T> setCacheList(String key, List<T> dataList) { ListOperations listOperation = redisTemplate.opsForList(); if (null != dataList) { int size = dataList.size(); for (int i = 0; i < size; i++) { listOperation.leftPush(key, dataList.get(i)); } } return listOperation; } /** * 获得缓存的list对象 * @param key 缓存的键值 * @return 缓存键值对应的数据 */ public <T> List<T> getCacheList(String key) { List<T> dataList = new ArrayList<T>(); ListOperations<String, T> listOperation = redisTemplate.opsForList(); Long size = listOperation.size(key); for (int i = 0; i < size; i++) { dataList.add(listOperation.index(key, i)); } return dataList; } /** * 缓存Set * @param key 缓存键值 * @param dataSet 缓存的数据 * @return 缓存数据的对象 */ public <T> BoundSetOperations<String, T> setCacheSet(String key, Set<T> dataSet) { BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key); Iterator<T> it = dataSet.iterator(); while (it.hasNext()) { setOperation.add(it.next()); } return setOperation; } /** * 获得缓存的set * @param key * @return */ public <T> Set<T> getCacheSet(String key) { Set<T> dataSet = new HashSet<T>(); BoundSetOperations<String, T> operation = redisTemplate.boundSetOps(key); dataSet = operation.members(); return dataSet; } /** * 缓存Map * @param key * @param dataMap * @return */ public <T> HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap) { HashOperations hashOperations = redisTemplate.opsForHash(); if (null != dataMap) { for (Map.Entry<String, T> entry : dataMap.entrySet()) { hashOperations.put(key, entry.getKey(), entry.getValue()); } } return hashOperations; } /** * 获得缓存的Map * @param key * @return */ public <T> Map<String, T> getCacheMap(String key) { Map<String, T> map = redisTemplate.opsForHash().entries(key); return map; } /** * 获得缓存的基本对象列表 * @param pattern 字符串前缀 * @return 对象列表 */ public Collection<String> keys(String pattern) { return redisTemplate.keys(pattern); } }

2.5 controller

2.5.1 UserController

import com.ldx.redis.entity.SysUser; import com.ldx.redis.service.SysUserService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.util.List; /** * 用户管理 * @author ludangxin * @date 2021/8/11 */ @RestController @RequestMapping("user") @RequiredArgsConstructor public class UserController { private final SysUserService userService; @GetMapping public List<SysUser> queryAll() { return userService.queryAll(); } @GetMapping("{userId}") public SysUser getUserInfo(@PathVariable Long userId) { return userService.getUserInfo(userId); } @PostMapping public String add(@RequestBody SysUser user) { userService.add(user); return "新增成功~"; } @PutMapping("{userId}") public String update(@PathVariable Long userId, @RequestBody SysUser user) { userService.update(userId, user); return "更新成功~"; } @DeleteMapping("{userId}") public String del(@PathVariable Long userId) { userService.delete(userId); return "删除成功~"; } }

2.5.2 RoleController

import com.ldx.redis.entity.SysRole; import com.ldx.redis.service.SysRoleService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.util.List; /** * 角色管理 * @author ludangxin * @date 2021/8/12 */ @RestController @RequestMapping("role") @RequiredArgsConstructor public class RoleController { private final SysRoleService roleService; @GetMapping public List<SysRole> queryAll() { return roleService.queryAll(); } @GetMapping("{roleId}") public SysRole getUserInfo(@PathVariable Long roleId) { return roleService.getRoleInfo(roleId); } @PostMapping public String add(@RequestBody SysRole role) { roleService.add(role); return "新增成功~"; } @PutMapping("{roleId}") public String update(@PathVariable Long roleId, @RequestBody SysRole role) { roleService.update(roleId, role); return "更新成功~"; } @DeleteMapping("{roleId}") public String del(@PathVariable Long roleId) { roleService.delete(roleId); return "删除成功~"; } }

2.6 service.impl

2.6.1 UserServiceImpl

优雅的使用redis注解实现对数据的缓存

@Cacheable:unless:当unless成立时则不缓存。这里判断size主要是不想将空值存入redis。

CacheConstant.USER_CACHE_KEY_PREFIX + "' + #userId":其key = 指定前缀 + 当前方法实参(userId)。

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.ldx.redis.constant.CacheConstant; import com.ldx.redis.entity.SysUser; import com.ldx.redis.mapper.SysUserMapper; import com.ldx.redis.service.SysUserService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; import org.springframework.stereotype.Service; import java.util.List; /** * 用户管理实现 * @author ludangxin * @date 2021/8/11 */ @Slf4j @Service @RequiredArgsConstructor @CacheConfig(cacheNames = CacheConstant.USER_CACHE_NAME) public class SysUserServiceImpl implements SysUserService { private final SysUserMapper userMapper; @Override @Cacheable(key = "'" + CacheConstant.USER_CACHE_KEY_PREFIX + "all'", unless = "#result.size() == 0") public List<SysUser> queryAll() { log.debug("查询全部用户信息~"); LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>(); return userMapper.selectList(queryWrapper); } @Override @Cacheable(key = "'" + CacheConstant.USER_CACHE_KEY_PREFIX + "' + #userId", unless = "#result == null") public SysUser getUserInfo(Long userId) { log.debug("查询用户:{} 详情", userId); return userMapper.selectById(userId); } @Override @CacheEvict(key = "'" + CacheConstant.USER_CACHE_KEY_PREFIX + "all'") public void add(SysUser user) { log.debug("新增用户:{}", user.getNickName()); userMapper.insert(user); } @Override @Caching(evict = {@CacheEvict(key = "'" + CacheConstant.USER_CACHE_KEY_PREFIX + "all'"), @CacheEvict(key = "'" + CacheConstant.USER_CACHE_KEY_PREFIX + "' + #userId") }) public void update(Long userId, SysUser user) { log.debug("更新用户:{}", user.getNickName()); user.setId(userId); userMapper.updateById(user); } @Override @Caching(evict = {@CacheEvict(key = "'" + CacheConstant.USER_CACHE_KEY_PREFIX + "all'"), @CacheEvict(key = "'" + CacheConstant.USER_CACHE_KEY_PREFIX + "' + #userId") }) public void delete(Long userId) { log.debug("删除用户:{}", userId); userMapper.deleteById(userId); } }

2.6.2 SysRoleServiceImpl

使用redis工具类实现对数据的缓存。

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.ldx.redis.constant.CacheConstant; import com.ldx.redis.entity.SysRole; import com.ldx.redis.mapper.SysRoleMapper; import com.ldx.redis.service.SysRoleService; import com.ldx.redis.util.RedisUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import java.util.Collections; import java.util.List; import java.util.Objects; /** * 角色管理 * @author ludangxin * @date 2021/8/11 */ @Slf4j @Service @RequiredArgsConstructor public class SysRoleServiceImpl implements SysRoleService { private final SysRoleMapper roleMapper; private final RedisUtil redisUtil; String allKey = CacheConstant.getRoleCacheKey("all"); @Override public List<SysRole> queryAll() { List<SysRole> roles = redisUtil.getCacheList(allKey); if(!CollectionUtils.isEmpty(roles)) { return roles; } log.debug("查询全部角色信息~"); LambdaQueryWrapper<SysRole> queryWrapper = new LambdaQueryWrapper<>(); List<SysRole> sysRoles = roleMapper.selectList(queryWrapper); if(CollectionUtils.isEmpty(sysRoles)) { return Collections.emptyList(); } redisUtil.setCacheList(allKey, sysRoles); return sysRoles; } @Override public SysRole getRoleInfo(Long roleId) { String roleCacheKey = CacheConstant.getRoleCacheKey(String.valueOf(roleId)); SysRole role = redisUtil.getCacheObject(roleCacheKey); if(Objects.nonNull(role)) { return role; } log.debug("查询角色:{} 详情", roleId); SysRole sysRole = roleMapper.selectById(roleId); if(Objects.isNull(sysRole)) { return null; } redisUtil.setCacheObject(roleCacheKey, sysRole); return sysRole; } @Override public void add(SysRole role) { log.debug("新增角色:{}", role.getName()); roleMapper.insert(role); redisUtil.deleteObject(allKey); } @Override public void update(Long roleId, SysRole role) { log.debug("更新角色:{}", role.getName()); String roleCacheKey = CacheConstant.getRoleCacheKey(String.valueOf(roleId)); role.setId(roleId); roleMapper.updateById(role); // 更新缓存 redisUtil.setCacheObject(roleCacheKey,role); // 清除缓存 redisUtil.deleteObject(allKey); } @Override public void delete(Long roleId) { log.debug("删除角色:{}", roleId); roleMapper.deleteById(roleId); // 清除缓存 redisUtil.deleteObject(CacheConstant.getRoleCacheKey(String.valueOf(roleId))); redisUtil.deleteObject(allKey); } }

2.7 启动测试

这里只测试了user模块(都测试并且贴图会显得篇幅太长且繁琐),role模块本人测试后结果正确。

查询列表:

​ 调用接口返回全部数据并缓存完成,再次调用无查询日志输出,符合预期。

​ 接口调用:

​ 查看缓存:

查看用户详情:

​ 接口调用返回用户详情信息并缓存完成,再次调用无查询日志输出,符合预期。

​ 接口调用:

​ 查看缓存:

更新数据:

​ 接口调用返回更新成功,并且查看全部的缓存被清除。符合预期。

​ 接口调用:

​ 查看缓存:

3. 内置缓存注解

3.1 @CacheConfig

@Cacheable()里面都有一个value=“xxx”的属性,这显然如果方法多了,写起来也是挺累的,如果可以一次性声明完 那就省事了, 所以,有了@CacheConfig这个配置,@CacheConfig is a class-level annotation that allows to share the cache names,如果你在你的方法写别的名字,那么依然以方法的名字为准。

3.2 @Cacheable

@Cacheable(value="myCache"),这个注释的意思是,当调用这个方法的时候,会从一个名叫myCache 的缓存中查询,如果没有,则执行实际的方法(即查询数据库),并将执行的结果存入缓存中,否则返回缓存中的对象。

3.3 @CachePut

@CachePut 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用。

3.4 @CacheEvict

@CachEvict 的作用 主要针对方法配置,能够根据一定的条件对缓存进行清空。

// 清空当前cache name下的所有key @CachEvict(allEntries = true)

3.5 @Caching

@Caching可以使注解组合使用,比如根据id查询用户信息,查询完的结果为{key = id,value = userInfo},但我们现在为了方遍,想用用户的手机号,邮箱等缓存对应用户的信息,这时候我们就要使用@Caching。例:

@Caching(put = { @CachePut(value = "user", key = "#user.id"), @CachePut(value = "user", key = "#user.username"), @CachePut(value = "user", key = "#user.email") }) public User getUserInfo(User user){ ... return user; }

__EOF__

本文作者张铁牛
本文链接https://www.cnblogs.com/ludangxin/p/15139522.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   张铁牛  阅读(5278)  评论(2编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示
点击右上角即可分享
微信分享提示