黑马点评redis改 part 1
本篇将主要阐述短信登录的相关知识,感谢黑马程序员开源,感谢提供初始源文件(给到的是实战第7集开始的代码)【Redis实战篇】黑马点评学习笔记(16万字超详细、Redis实战项目学习必看、欢迎点赞⭐收藏)-CSDN博客
1.打开localhost_3306,选中右击“新建数据库”
2.指定数据库名和字符集(可根据sql文件的字符集类型自行选择)
3.选中数据库下的表运行SQL文件
其实我想发在这里的,但是1285行代码太多了
4.选中路径导入
将hmdp.sql导入(本人是mysql8.0.27版本),即可看到包括tb_user:用户表,tb_user_info:用户详情表,tb_shop:商户信息表,tb_shop_type:商户类型表,tb_blog:用户日记表(达人探店日记),tb_follow:用户关注表,tb_voucher:优惠券表,tb_voucher_order:优惠券的订单表 的一共11个表
在资料中提供了一个项目源码,hm-dianping,大概看一下,经典的ssm,一眼springboot。修改application.yaml部分,对照自己 的即可
server:port: 8081
spring:application:name: hmdpdatasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/hmdp?useSSL=false&serverTimezone=UTCusername: rootpassword: rootredis:host: 192.168.169.133port: 6379password: 123321lettuce:pool:max-active: 10max-idle: 10min-idle: 1time-between-eviction-runs: 10sjackson:default-property-inclusion: non_null # JSON处理时忽略非空字段
mybatis-plus:type-aliases-package: com.hmdp.entity # 别名扫描包
logging:level:com.hmdp: debug
RedissonConfig中也有redis的地址需要修改
alt+8打开service,添加 “运行配置类型” springboot。成功运行!
运行前端项目
在nginx所在目录下打开一个cmd窗口
start nginx.exe
打开浏览器的手机模式和本地的8080端口即可
基于session实现登录
我们在http://localhost:8080/login.html输入一个合法的手机号码可以看到一个
已完成加载:POST "http://localhost:8080/api/user/code?phone=16883577632"。 请求发到api的user
请求方式POST,请求路径/user/code,请求参数phone、电话号码,返回值无
我们要打开UserController,实现发送手机验证码的功能,由于中国大陆的手机号政策,实际上你可以改为邮箱验证,毕竟只是一个简单demo而已。
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {// 发送短信验证码并保存验证码return userService.sendCode(phone, session);
}
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result sendCode(String phone, HttpSession session) {// 1.校验手机号if (RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回错误信息return Result.fail("手机号格式错误!");}// 3.符合,生成验证码String code = RandomUtil.randomNumbers(6);// 4.保存验证码到 session //这里推荐使用手机号作为key,验证码作为值session.setAttribute("code",code);// 5.发送验证码,通过aliyun那些短信平台实现log.debug("发送短信验证码成功,验证码:{}", code);// 返回okreturn Result.ok();}
这时后台可以直接看到发送短信验证码成功,验证码245333
我们仔细看login功能,前端发送的是json格式,所以需要RequestBody解析下,loginFormDTO格式里面包括三个要素,接下来进一步完善controller
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){// 实现登录功能return userService.login(loginForm, session);
}
UserServiceImpl.java修改如下,这里有实际上有一个小保险,发送验证码时应该将手机号保存在session中,在登录时验证是否当前手机号是否是发送验证码的手机号,否则先用自己手机号发送验证码,再用别人手机号登录。总之就是登录需要校验此手机号和发送验证码的手机号是同一个, 你乐意的话可以加个ip地址校验不过不太好使唤
数据库在中tb_user中有nick_name字段,手机号什么的,这里用lambdaquery的朋友注意了,mp版本要3.5,用老师的这个版本查询为空的时候会报错
@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {// 1.校验手机号String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回错误信息return Result.fail("手机号格式错误!");}// 2.从redis获取验证码并校验Object cacheCode = session.getAttribute("code");String code = loginForm.getCode();if (cacheCode == null || !cacheCode.equals(code)) {// 3. 不一致,报错return Result.fail("验证码错误");}// 4.一致,根据手机号查询用户 select * from tb_user where phone = ?User user = query().eq("phone", phone).one();// 5.判断用户是否存在if (user == null) {// 6.不存在,创建新用户并保存user = createUserWithPhone(phone);}session.setAttribute("user",user);return Result.ok()//实际上只需要return null,session就直接写到你的cookie中了}private User createUserWithPhone(String phone) {// 1.创建用户User user = new User();user.setPhone(phone);user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));// 2.保存用户save(user);return user;}
Optional.ofNullable(lambdaQuery().eq(User::getPhone, phone).one()).orElseGet(() -> createUserWithPho
我们在前端,登录后可以跳转一下,但是没有做登录校验功能,你在数据库可以查找到对应的数据
登陆验证功能
事实上的登录验证呢就是这样的一个请求,这个userme查询当前所在的用户信息,如果你能return,那么就成功了.但是这里有点问题,我们在黑马点评里面有很多很多control,其中刚才讲那个userme登录校验属于usercontrol,前端向usercontrol发请求,里面编写这一堆的业务逻辑。但是呢后续随着业务的开发,越来越多的业务都需要去校验用户的登录,显然不能写一堆control。这也是拦截器的由来,所有请求啊都必须先经过拦截器,再由拦截器判断该不该放行到达control
拦截器确实可以帮助 我们实现对用户登录的校验,在其他业务中人家是需要这个用户信息的,校验这是拿到了,所以需要把这个拦截器里拦截得到的用户信息传递到control里面去。而且在传递的过程中需要注意slocal解决线程的安全问题,拦截器拦截信息后保存在slocal(线程序对象)每一个进入tomcat的请求都是一个独立的线程,slocal在每个线程内开辟一个内存的空间保存对应的用户,每个线程互不干扰。
可能是放在Session里你要用的话,这个session参数你要一直传下去,ThreadLocal调用一个API就能实现你说哪个好?
拦截器可以写在utils里面,叫做LoginInterceptor.java
package com.hmdp.utils;import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.获取sessionHttpSession session= request.getSession();//2.获取session中的用户Object user = session.getAttribute( s: "user");//3.判断用户是否存在if (user == null){//4.不存在,拦截,返回401状态码response.setStatus(401);return false;} //5.存在,保存用户信息到ThreadLocal//6.放行return true;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {UserHolder.removeUser();}
}
因为ThreadLocal底层是ThreadLocalMap,当期线程Threadlocal作为key(弱引用),user作为value(强引用) 这里涉及到了ThreadLocal的相关知识,不懂为啥要移除,避免内存泄漏的,建议查询资料
ThreadLocal维护了一个ThreadLocalMap,在map中的Entry继承了WeakReference,其中key为使用了弱引用的ThreadLocal实例,注意这里他发给我们的是UserDTO我们需要创建一个DTO对象(详见userholder)然后进行属性拷贝、不可以直接强转不然会报可能为空的错;移除用户是因为:因为ThreadLocal对应的是一个线程的数据,每次http请求,tomcat都会创建一个新的线程,也就是说,当前的ThreadLocal只在当前的线程中有用;jvm不会把强引用的value回收掉,所以value没被释放;
要想让拦截器生效还要配置拦截器,在config中新建文件MvcConfig,去掉code.login等等等等
package com.hmdp.config;import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import javax.annotation.Resource;@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 登录拦截器registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login").order(1);// token刷新的拦截器registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);}
}
做登录校验用到那个叫user/me的一个接口,这个接口最终还需要把当前登录的用户信息返回到前端,拦截器已经把用户放到了userholder里面去了,所以只需要userholder.get即可了
@GetMapping("/me")public Result me(){// 获取当前登录的用户并返回UserDTO user = UserHolder.getUser();return Result.ok(user);}
现在可能会出bug,继续做下一集就ok了
登录校验功能返回的信息有点多,注意:跳转到主页的,需要去修改前端代码,改为跳转到个人详情页
注意:跳转到主页的,需要去修改前端代码,改为跳转到个人详情页,直接跳到首页并且点击我的
或者跳回首页的 可以看看前端login部分是不是没有跳到info而是去index了
需要从新登录的在login这里下面返回的改成这个Result.ok(userService.login(loginForm,session))
我们回到me方法,从userholder里得到用户以后就直接返回了,其实也说明取出的信息就是完整的信息,这个消息是拦截器那个session存储的,随着时间推移里面的信息越来越多,也就说明压力也大,其中谁给session信息呢?就是login啊于是就这样了
UserServiceImpl.java
..............
//7.保存用户信息到session中
session.setAttribute("user",Beanutil.copyProperties(user,UserDTO.class));
return Result.ok();那么拦截器的对象也就是UserDTO对象了
LoginInterceptor.java
................
//5.存在,保存用户信息到ThreadLocal
UserHolder.saveUser((UserDTO) user);
//6.放行
return true;顺道再UserHolder里面改为dto
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();public static void saveUser(UserDTO user){tl.set(user);
}
很多很多依赖都改成userdto
BlogController UserController通通改成UserDTO user
集群的session共享问题
session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。
早期的解决方案是tomcat之间配置拷贝,但是有几个问题,拷贝耗内存,并且有延迟
session的替代方案应该满足:1.数据共享 2.内存存储 3.key、value结构
基于Redis实现共享session登录
redis作为key--value来说,redis 是一个共享的一个内存空间,不管是谁来发请求,在我们服务端是不是只有一个release,大家都往里面去存。如果你的手机号来的时候用code啊,有一个手机又一个code,那么不同的手机号都用code为key,互相就覆盖; 那么这个验证码将来是不是就丢失了很多,很多人就登录不上。我们必须确保每一个不同的手机号验证保存的key是不一样的。
手机号作为key 验证码做为value,现在 是redis,没有原来的自动每一次 请求都会带着Session ID来。现在是客户端还得带着这个信息来取才能验证。那么这样一来我们去校验的时候,可以基于手机号为key,从redis去读取到啊这个验证码然后跟他提交的验证码做比较就行了(这里解决了前面 的类bug:发送和登录手机号不一致的问题)
第二要考虑的就是我们这个key,保存验证码的时候我们用的是string类型,因为他大部分是六位数的的数字,用了字符串形式去保存。但在这里呢你保存的是一个用户的对象,保存对象我们应该选择哪种数据类型
当我们在redis中保存对象时一般两种结构,第一种是string结构,第二种是hash: string其实就是把我们的java对象序列化为json的字符串
hash那它的value啊是一个哈希,可以理解为map,它其实就是把我们的java对象中的每一个字段都作为这个value中的一个field和value,string把整个数据变成一个串,而哈希结构呢每个字段是独立的,所以说它可以针对单个字段做crud
对于key的要求:1.保证唯一2.客户端将来能够去携带这样一个呢key方便从redis里再去取出这个值。
不一样的 这个项目就是学redis可以不能用jwt啊 jwt就是后端不存储,直接根据jwt解析。
前端登录页面中是用一个axiou的请求啊来去做,在这个请求的响应里面,这个data其实就是我们要返回到前端的这样登录凭证token,它会把它保存在session storage里。在我们前端的commonjs里还有这么一点逻辑:就是从session storage里得到这个token,下边是一个拦截器,而每次发请求都会执行这样一段逻辑。token作为这个请求头,这个头的名字叫authorization,确保以后凡是有axios发起的这种请求都会携带authorization这个头,在服务端就能获取这个头,实现登陆验证
现在修改代码,只有修改验证码发生变化不再是保存到redis时这个key啊不再是code,而是以手机号为key好
@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result sendCode(String phone, HttpSession session) {// 1.校验手机号if (RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回错误信息return Result.fail("手机号格式错误!");}// 3.符合,生成验证码String code = RandomUtil.randomNumbers(6);// 4.保存验证码到 sessionstringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);// 5.发送验证码log.debug("发送短信验证码成功,验证码:{}", code);// 返回okreturn Result.ok();}
其中在util中新建RedisConstants文件来定义
package com.hmdp.utils;public class RedisConstants {public static final String LOGIN_CODE_KEY = "login:code:";public static final Long LOGIN_CODE_TTL = 2L;
}
autowired和resource的功能类似只不过autowired是先找类型再找名字,resource是先找名字再找类型,接下来写短信功能
@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {// 1.校验手机号String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回错误信息return Result.fail("手机号格式错误!");}// 3.从redis获取验证码并校验String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);String code = loginForm.getCode();if (cacheCode == null || !cacheCode.equals(code)) {// 不一致,报错return Result.fail("验证码错误");}// 4.一致,根据手机号查询用户 select * from tb_user where phone = ?User user = query().eq("phone", phone).one();// 5.判断用户是否存在if (user == null) {// 6.不存在,创建新用户并保存user = createUserWithPhone(phone);}// 7.保存用户信息到 redis中// 7.1.随机生成token,作为登录令牌String token = UUID.randomUUID().toString(true);// 7.2.将User对象转为HashMap存储UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));// 7.3.存储String tokenKey = LOGIN_USER_KEY + token;stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);// 7.4.设置token有效期stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.返回tokenreturn Result.ok(token);}
在实现用户登录功能时,首先需要生成一个随机token作为用户身份凭证。这里建议使用UUID(通用唯一识别码),因其具备高唯一性和简便性。具体可采用Hutool工具库提供的UUID方法生成不含中划线的简洁字符串,如UUID.randomUUID().toString(true)
。生成token后,需将其作为Redis的key,将用户信息以哈希结构存储。为避免多次与Redis交互,应通过BeanUtil工具将UserDTO对象转换为Map,利用putAll
方法一次性存入多个字段。存储时需注意为key添加业务前缀(如login:user:token
),并设置30分钟的有效期,防止内存过度占用。具体实现步骤为:校验手机号格式,比对Redis中存储的验证码,查询或创建用户,生成token,转换用户数据为Map结构,存入Redis并设置过期时间,最终返回token给前端。其中,对象转换需使用BeanUtil.copyProperties
和BeanUtil.beanToMap
方法,同时忽略空值字段并统一字段值类型,确保Redis存储结构的规范性。同样的redisconstants修改
package com.hmdp.utils;public class RedisConstants {public static final String LOGIN_CODE_KEY = "login:code:";public static final Long LOGIN_CODE_TTL = 2L;public static final String LOGIN_USER_KEY = "login:token:";public static final Long LOGIN_USER_TTL = 36000L;
}
接下来,我们需要对代码中的变量名进行调整。比如原来的login_code
现在不再需要了,可以将其改为login_user
,因为这是与用户登录相关的业务。相应的,Redis的key前缀也可以命名为login:user:key
,而token的有效期则设置为30分钟。这里的token名称可以叫login_token
或者user_token
,都是可以接受的。然而,仅仅设置30分钟的有效期还不够。目前的逻辑是,从用户登录那一刻开始计时,30分钟后无论用户是否活跃,Redis都会将该用户的登录状态移除。这显然不符合实际需求,因为我们希望的是:只要用户持续访问系统,token的有效期就应该不断刷新,而不是在固定时间后强制失效。
那么问题来了:如何判断用户是否在访问系统?其实,我们之前实现过一个功能——登录拦截器。所有的请求进入系统时,都会经过这个拦截器的校验。如果请求通过了校验,就说明两点:第一,该用户已经登录;第二,该用户当前处于活跃状态。基于这两点,我们可以在拦截器中添加一个逻辑:每次用户访问系统时,更新Redis中对应token的有效期。这样一来,只要用户持续访问系统,token的有效期就会不断延长,只有当用户超过30分钟没有任何操作时,token才会被移除。
因此,在修改登录状态校验的业务逻辑时,我们需要在原有逻辑的基础上增加一个新功能:更新token有效期。接下来,我们可以找到与登录相关的业务代码,这部分逻辑写在拦截器(LoginInterceptor
)中。
public class LoginInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.判断是否需要拦截(ThreadLocal中是否有用户)if (UserHolder.getUser() == null) {// 没有,需要拦截,设置状态码response.setStatus(401);// 拦截return false;}// 有用户,则放行return true;}
}
在这个地方,我们无法使用@Autowired
、@Resource
等注解来进行依赖注入,而只能通过构造函数的方式来实现依赖注入。这是因为当前类的对象是我们手动通过new
关键字创建的,而不是由Spring容器管理的。换句话说,这个类的对象并没有通过@Component
或其他类似的注解交给Spring来创建和管理,因此Spring无法自动为我们完成依赖注入。对于Spring管理的对象,比如添加了@Autowired
注解的类,Spring会自动完成依赖注入;但如果我们手动创建对象,则没有任何机制能够帮助我们完成依赖注入,也就无法使用@Resource
等注解。
那么在这种情况下,我们选择通过构造函数注入的方式解决问题。那么谁来负责为我们注入依赖呢?这就需要看是谁在使用这个类了。回顾一下,我们在MvcConfig
配置类中的拦截器部分使用了这个类,而这里报错了,说明我们需要对这部分代码进行调整。解决方法是,在MvcConfig
中获取RedisTemplate
实例。大家可以看到,MvcConfig
类上添加了@Configuration
注解,这意味着这个类是由Spring来构建和管理的。既然是由Spring管理的类,就可以利用Spring的依赖注入功能,因此我们可以通过@Resource
注解直接获取StringRedisTemplate
实例,从而完成依赖注入。
所以把这个手动new的换成@Component,就可以用自动装配了;但是不能加Competent,拦截器是一个非常轻量级的组件,只有在需要时才会被调用,并且不需要像控制器或服务一样在整个应用程序中可用。因此,将拦截器声明为一个Spring Bean可能会引导致性能下降。
那MvcConfig这里怎么获取redis template?这个类加了configuration注解说明这个类将来是不是由spring构建的,由spring来构建这个类的对象他就可以做依赖注入,因此可以利用resource注解来获取string redis template啊
package com.hmdp.config;import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import javax.annotation.Resource;@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 登录拦截器registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login").order(1);}}
}
回到LoginInterceptor,这里呢就拿到了redistemplate了
package com.hmdp.utils;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;public class LoginInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求头中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {// 不存在,拦截,返回401状态码response.setStatus(401);return false;}// 2.基于TOKEN获取redis中的用户String key = RedisConstants.LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
// 3.判断用户是否存在if (userMap.isEmpty()) {// 4.不存在,拦截,返回401状态码response.setStatus(401);return false;}// 5.将查询到的Hash数据转为UserDTO对象UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(),false);// 6.存在,保存用户信息到 ThreadLocalUserHolder.saveUser(userDTO);// 7.刷新token有效期stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserHolder.removeUser();}
}
这一步放在mvcconfig里好一点吧,不然走被拦截的请求就不更新了;启动报错 BeanCreationException的记得给@Resource 的名字改为stringRedisTemplate--或者将注解改为@Autowied这里跟注解的特性有关不多解释了
java.lang.Long cannot be cast to java.lang.String
输入登录,会出现报错了(服务器错误),把我们的usermap向redistemplate写的时候报错了:类型转换long不能转化为string,那么userdto里其实只有id是long类型对吧,redis无法存储。为什么?redis template,string template它有一个什么特点,他要求你的key或者value都是string结构,而我们把数据转成map的时候,我们那个字段id是long类型。
因此确保这里边的每一个值都要以string的形式存储的,是map的key和value都得是string结构。有两种方法,
第一种笨办法,自己new一个map,不再Map<String ,Object> userMap = BeanUtil.beanToMap(userDTO);然后把这个对象里面的字段名作为key;
第二种,Objectbean,Map<String,object>targetMap,CopyOptionscopyOptions ,允许你对key和value做自定义
@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {// 1.校验手机号String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {// 2.如果不符合,返回错误信息return Result.fail("手机号格式错误!");}// 3.从redis获取验证码并校验String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);String code = loginForm.getCode();if (cacheCode == null || !cacheCode.equals(code)) {// 不一致,报错return Result.fail("验证码错误");}// 4.一致,根据手机号查询用户 select * from tb_user where phone = ?User user = query().eq("phone", phone).one();// 5.判断用户是否存在if (user == null) {// 6.不存在,创建新用户并保存user = createUserWithPhone(phone);}// 7.保存用户信息到 redis中// 7.1.随机生成token,作为登录令牌String token = UUID.randomUUID().toString(true);// 7.2.将User对象转为HashMap存储UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
/*copyoption就是做数据拷贝时的一个选项,这样就创建出来一个copyoption了。
但是呢这个地方创建出来是默认的,我们要自定义允许你做各种各样的set,比如说呢set
ignore null value就是忽略一些空的值*/// 7.3.存储String tokenKey = LOGIN_USER_KEY + token;stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);// 7.4.设置token有效期stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.返回tokenreturn Result.ok(token);}
问题就是StringRedisTemplate是定义一个String类型的key和value,但是再map转换成user的时候无法将string转换成long,所以需要这种方式或者自定义一个map.
转化下map值类型 userMap.forEach((key,value)->{if(null!=value) userMap.put(key, String.valueOf(value)); });
登录后又回来的,记得在login业务里返回token到控制层(要重新登录的看一下是不是login方法ok里有没有返回token 所以拦截器中就没有获取到 就给你打回了)controller层的login是retuen userService.login(loginForm,session)
我们的登录功能是基于拦截器做的校验对吧?没有请求进入了拦截器以后,我们会尝试去获取请求头中的 token。那么如果说他之前登录过他的头里一定会有token对不对?那么我们再去根据token到redis里查询对应的用户信息,那么查了以后对用户做一个判断,存在或者是不存在,不存在给你拦截,存在我就继续,继续干什么?用户存在,我就会把它保存到所有的local当中,方便后续的control的业务去使用它,对吧?好,那么保存完了,我们还做了一件事,就是去刷新token的有效期,为什么?因为我们在redis里保存的 Token有效期是30分钟,如果说不去做刷新,用户30分钟后就可能失去了登录状态了,这个就不太友好。
所以我们去做一个刷新,每当用户来访问,我们都会去刷新一次,确保只要用户一直在操作,那么它这个token就不会消失。好,这是我们刷新token的一个目的,那么最后放行就可以了。但是我们现在能不能真正的达成,说是只要用户一直在访问就不会过期,还不太行,为什么?因为拦截器它拦截的路径不是一切路径,它拦的是那些需要做登录校验的路径。
比如说我们的userme,再比如说将来用户的下单支付等等这样的一些对用户信息有需求的路径,或者说被拦截器拦截的路径,但它不是拦截一切。所以这就导致了如果说,我们的用户一直访问的是不需要登录的这样的一些页面。举个例子,我们的首页,商户的详情页,那么这些都是不需要登录就能看的,那么这样拦截器就不生效,那么它就不会去刷新。接下来如果说30分钟以后,尽管用户一直在访问,用户的登录是不是就也消失了,所以这是不太合理的一个点,针对这个点我们该怎么优化,我们可以这么来做。
这里意思是如果你登录了,但是你访问的是主页,主页不需要拦截,既然不能刷新token,就在你看首页或者商家的时候你突然下单,这时token过期就失败了
在原有这个拦截器的基础上,再加一个新的拦截器,这样用户请求就要先经过第一个连接器,再经过第二个。因为什么?我们的第二个连接器它拦截的是需要登录的那些东西,而不是所有的路径,所以没有办法给所有的请求都做刷新,对不对?我在新加这个连线我就让他干什么?拦截一切路径。也就是说所有请求都会经过我,我是不是可以在拦截器里来做刷新token有效期的动作?
我在这里获取token,获取rest的用户。当然了有的时候你查的时候说万一不存在怎么办?好不存在我放行我不管,只要你存在,我就给你保存到所有logo做刷新的动作。也就是说我这里不做拦截,我这个拦截器虽然是拦截一些路径,但是唯一目的其实就是保存了所有logo和刷新的动作。
好,那么这样是不是可以确保一切请求都会触发创新的动作?拦截的动作在哪做?在第二个拦截器里,在第二个拦截器里我就不用重复上面这5步了,我只需要从ThreadLocal里面查,因为你这个来写已经把它保存到算了对吧?我去查查了以后,如果不存在我就拦截,如果存在我是不是就可以放行了?那也就是第一个联系它的核心工作就是得到用户保存起来,并且刷新。 那么第二个引起的核心动作才是做登录拦截,两个分工这个问题就得到解决了。在utils中新建一个RefreshTokenInterceptor
package com.hmdp.utils;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;public class RefreshTokenInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求头中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {return true;/*此处return true是对的,若return false,第一次访问登录页面时就会被拦截;
若return true,第一次访问登录页会进入Login拦截器,由于登录页为放行路径,放行*/}// 2.基于TOKEN获取redis中的用户String key = LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);// 3.判断用户是否存在if (userMap.isEmpty()) {return true;}// 5.将查询到的hash数据转为UserDTOUserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);// 6.存在,保存用户信息到 ThreadLocalUserHolder.saveUser(userDTO);// 7.刷新token有效期stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserHolder.removeUser();}
}
修改LoginInterceptor
package com.hmdp.utils;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;public class LoginInterceptor implements HandlerInterceptor {private StringRedisTemplate stringRedisTemplate;public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.判断是否需要拦截(ThreadLocal中是否有用户)if (UserHolder.getUser() == null) {//没有,需要拦截,设置状态码response.setStatus(401);//拦截return false;//有用户,则放行}/*从登录拦截器的名字LoginInterceptor就能看出其实人家只需要做一件事 就是判断线程中有没有用户就可以了 其他事情交给其他类做*/return true;}
}
我们希望的是refresh先执行,只有他先执行了拿到我们的用户保存到了sever local,那么在拦截才能去做拦截的判断,是不是这样子?所以说这两个其实是有个先后顺序的,那么我们怎么控制拦截器的执行顺序呢?事实上在我们这个地方我们添加拦截器的时候,大家可以根据看一眼,在我们添加拦截器的时候,拦截器其实会被注册成一个东西叫 intercept registration. 就是注册器。
那么注册器里面其实有一个什么东西,有一个order,就是来仪器的执行顺序,在默认情况下,所有联系的顺序都是0,那都是0的情况下他们怎么执行的,按照添加顺序执行。 所以说如果简单来说的话,我们其实只需要干什么?先添加addInterceptor再添addInterceptor是不就ok了?但是如果你想控制的严谨一点,你就可以干什么?给他的order调的稍微小一点,然后给哥们的order调到什么大一点,因为值越大,执行的优先级反而越低,越小优先级是越高的,这样的话我们就可以确保什么?下面先执行上面那个后执行了。Ok,那么我们就把两个连接器添加完毕了,是登录拦截器,那么下边那个是token刷新的拦截器。
package com.hmdp.config;import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import javax.annotation.Resource;@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 登录拦截器registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login").order(1);// token刷新的拦截器registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);}
}
相关文章:
黑马点评redis改 part 1
本篇将主要阐述短信登录的相关知识,感谢黑马程序员开源,感谢提供初始源文件(给到的是实战第7集开始的代码)【Redis实战篇】黑马点评学习笔记(16万字超详细、Redis实战项目学习必看、欢迎点赞⭐收藏)-CSDN博…...
降维算法之t-SNE
t-SNE(t-Distributed Stochastic Neighbor Embedding)算法详解 先说理解: t-SNE(t-distributed Stochastic Neighbor Embedding)是一种用来“可视化高维数据”的降维方法,通俗来说,它就像一个…...
使用 .NET 9 和 Azure 构建云原生应用程序:有什么新功能?
随着 .NET 9 推出一系列以云为中心的增强功能,开发人员拥有比以往更多的工具来在 Azure 上创建可扩展、高性能的云原生应用程序。让我们深入了解 .NET 9 中的一些出色功能,这些功能使构建、部署和优化云应用程序变得更加容易,并附有示例以帮助…...
python基础-10-组织文件
文章目录 【README】【10】组织文件(复制移动删除重命名)【10.1】shutil模块(shell工具)【10.1.1】复制文件和文件夹【10.1.1.1】复制文件夹及其下文件-shutil.copytree 【10.1.2】文件和文件夹的移动与重命名【10.1.3】永久删除文件和文件夹【10.1.4】用…...
从代码学习深度学习 - LSTM PyTorch版
文章目录 前言一、数据加载与预处理1.1 代码实现1.2 功能解析二、LSTM介绍2.1 LSTM原理2.2 模型定义代码解析三、训练与预测3.1 训练逻辑代码解析3.2 可视化工具功能解析功能结果总结前言 深度学习中的循环神经网络(RNN)及其变种长短期记忆网络(LSTM)在处理序列数据(如文…...
linux gcc
一、常用编译选项 基本编译 gcc [input].c -o [output] 示例: gcc hello.c -o hello # 将 hello.c 编译为可执行文件 hello ./hello # 运行程序 分步编译 预处理:-E(生成 .i 文件) gcc -E hello.c -o hello…...
“一路有你”公益行携手《东方星动》走进湖南岳阳岑川镇中心小学
2025年4月2日,“一路有你”公益行携手《东方星动》走进湖南岳阳岑川镇,一场充满爱与温暖的捐赠仪式在岑川镇中心小学隆重举行。这是一场跨越千里的爱心捐赠,也是一场别开生面的国防教育,更是一场赋能提质的文化盛宴。 岑川镇地处湘…...
HTML语言的空值合并
HTML语言的空值合并 引言 在现代Web开发中,HTML(超文本标记语言)是构建网页的基础语言。随着前端技术的快速发展,开发者们面临着大量不同的工具和技术,尤其是在数据处理和用户交互方面。空值合并是一些编程语言中常用…...
并发上传及 JS 的单线程特性
1. JS 的单线程特性 JS 是单线程特性,这意味着所有代码都在一个线程上(即主线程)执行,同一时间只有一个任务在执行,其他任务都在等待。 这意味着即使有多个异步操作,它们的回调函数也会按顺序执行ÿ…...
c语言学习16——内存函数
内存函数 一、memcpy使用和模拟实现1.1参数1.2 使用1.3 模拟实现 二、memmove使用和模拟实现2.1 参数2.2 使用2.3 模拟实现 三、memset使用3.1 参数3.2 使用 四、memcmp使用4.1 参数4.2 使用 一、memcpy使用和模拟实现 1.1参数 因为内存中不知道存的是什么类型的地址ÿ…...
html5炫酷3D文字效果项目开发实践
炫酷3D文字效果项目开发实践 这里写目录标题 炫酷3D文字效果项目开发实践项目概述技术实现1. 基础HTML结构2. 核心CSS技术2.1 3D空间设置2.2 文字立体效果2.3 动画效果 3. 交互优化3.1 鼠标悬停效果3.2 光晕效果 性能优化考虑项目亮点总结 项目概述 在这个项目中,我…...
【前端】【tailwind】tailwind默认重置了样式
Tailwind CSS 自身已经内置了现代化的样式重置方案 —— Preflight(基于 modern-normalize),这意味着在大多数情况下不需要额外引入传统的 reset.css 或 normalize.css。但具体是否需要补充重置,取决于你的项目需求,以…...
学透Spring Boot — 009. Spring Boot的四种 Http 客户端
目录 常见的HttpClient Spring 提供的HttpClient RestTemplate Spring 提供的模板类 XXXTemplate RestTemplate的使用 RestTemplate的使用技巧 RestTemplate的问题 RestClient RestClinet的基本使用 RestClient的自动配置 RestClient 序列化对象 异常处理 onStatus …...
STM32单片机入门学习——第14节: [6-2] 定时器定时中断定时器外部时钟
写这个文章是用来学习的,记录一下我的学习过程。希望我能一直坚持下去,我只是一个小白,只是想好好学习,我知道这会很难,但我还是想去做! 本文写于:2025.04.04 STM32开发板学习——第14节: [6-2] 定时器定时中断&定时器外部时钟 前言开发…...
Python 元组
Python 元组 元组的介绍 元组和列表很像,都是用来保存多个数据使用一对小括号()来表示一个元组元组和列表的区别在于,列表是可变数据类型,而元组是不可变数据类型 nums (9, 4, 3, 1, 9, 7, 6, 9, 3, 9) print(nums) # (9, 4, 3, 1, 9, 7…...
Linux系统编程:进程管理、内存对比与树莓派应用
一、认识进程和线程,在Linux系统下查看系统中各进程的编号pid并终止一个进程pid 1.进程和线程 进程:操作系统分配资源(如内存、CPU时间片)的基本单位。每个进程有独立的内存空间,进程间通信需要较复杂的机制…...
JSON介绍
JSON 的核心特点 易读易写:纯文本格式,结构清晰(像“键值对”的集合)。通用性强:任何语言都能解析或生成 JSON。用途广泛:常用于前后端数据传输、配置文件、API 接口等。 &am…...
【Cursor】切换主题
右键顶部,把菜单栏勾上 首选项-主题-颜色主题 选择和喜欢的颜色主题即可,一般是“现代深色”...
【C++11(上)】—— 我与C++的不解之缘(三十)
一、C11 这里简单了解一下C发展好吧: C11是C的第二个大版本,也是自C98以来最重要的一个版本。 它引入了大量的更改,它曾被人们称为C0x,因为它被期待在2010年之前发布;但在2011年8月12日才被采纳。 C03到C11花了8年时间…...
python如何把列表中所有字符变成小写
在Python中,你可以使用列表推导式(list comprehension)结合字符串的.lower()方法,将列表中的所有字符串元素转换为小写。以下是一个示例: # 定义一个包含字符串的列表 strings ["Hello", "WORLD"…...
DEAP数据集介绍
DEAP数据集介绍 0. 数据集摘要1. 文件列表2. 文件详细信息2.1 Online_ratings2.2 Video_list2.3 Participant_ratings2.4 Participant_questionnaire2.5 Face_video.zip2.6 Data_original.zip2.7 Data_preprocessed_matlab.zip 和 Data_preprocessed_python.zip 3. References欢…...
基于RDK X3的“校史通“机器人:SLAM导航+智能交互,让校史馆活起来!
视频标题: 【校史馆の新晋顶流】RDK X3机器人:导览员看了直呼内卷 视频文案: 跑得贼稳团队用RDK X3整了个大活——给校史馆造了个"社牛"机器人! 基于RDK X3开发板实现智能导航与语音交互SLAM技术让机器人自主避障不…...
JavaScript基础-window.localStorage
在现代Web开发中,数据存储对于提升用户体验至关重要。window.localStorage 是一种简单而强大的客户端存储机制,允许网页以键值对的形式持久化保存数据。与 sessionStorage 不同,localStorage 中的数据不会因浏览器标签页关闭或刷新而丢失&…...
在航电系统中提高可靠性的嵌入式软件设计
1.总线余度设计 数据传输采用双余度总线设计,CANFD为主,RS485为备。发送方将相同的数据分别通过双总线来发送,接收方优先处理主线数据。由于总线上数据频率固定,可设置定时器监控主总线的数据,当定时器超时后ÿ…...
H.266/VVC SCC技术学习:块差分脉冲编码调整(block differential pulse coded modulation, BDPCM)
近年来,屏幕内容视频广泛用于多媒体应用,例如远程桌面,屏幕共享等。由于屏幕内容视频的特性与自然视频有较大区别,VVC中使用了帧内块复制(intra block copy, 即IBC), 调色板(Palette)࿰…...
网络编程—Socket套接字(TCP)
上篇文章: 网络编程—Socket套接字(UDP)https://blog.csdn.net/sniper_fandc/article/details/146923670?fromshareblogdetail&sharetypeblogdetail&sharerId146923670&sharereferPC&sharesourcesniper_fandc&sharefro…...
数据结构:二叉树(三)·(重点)
二叉树的存储结构 ⼆叉树⼀般可以使⽤两种结构存储,⼀种顺序结构,⼀种链式结构。 顺序结构 顺序结构存储就是使⽤数组来存储,⼀般使⽤数组只适合表⽰完全⼆叉树,因为不是完全⼆叉树会有 空间的浪费,完全⼆叉树更适合…...
StyleTTS 2:文本到语音(Text-to-Speech, TTS)模型
StyleTTS 2 是一种先进的文本到语音(Text-to-Speech, TTS)模型,通过结合风格扩散(style diffusion)和对抗训练(adversarial training),利用大规模语音语言模型(Speech La…...
痉挛性斜颈康复路,饮食要点来相助
痉挛性斜颈患者除了接受正规治疗,合理饮食对缓解症状、促进康复也至关重要。 高蛋白质食物是饮食中的重点。像鸡蛋,富含人体必需的氨基酸,其组成与人体组成模式接近,易于吸收。每天吃 1 - 2 个鸡蛋,能为身体补充修复肌…...
谷歌 Gemini 2.5 Pro 免费开放
2025 年 3 月 30 日,谷歌宣布将最新的 Gemini AI 旗舰模型 Gemini 2.5 Pro 免费向所有 Gemini 应用用户开放。以下是关于此次免费开放的一些具体信息1: 背景:此前,Gemini 2.5 Pro 仅向支付 19.99 美元月费的 Gemini Advanced 用户…...
规则引擎Drools
1.规则引擎概述 1.1 什么是规则引擎 规则引擎 全称为业务规则管理系统,英文名为BRMS,规则引擎的主要思想是将应用程序中的业务决策部分分离出来,并使用预定义的语义模块编写业务规则,由用户或开发者在需要时进行配置和管理。 需…...
第三季:挪威
挪威 挪威是北欧的一个国家,位于斯堪的纳维亚半岛的西部。以下是关于挪威的详细介绍: 地理位置与自然环境 位置:挪威位于北欧,东邻瑞典,东北与芬兰和俄罗斯接壤,西濒挪威海,北临巴伦支海。地…...
搜索与图论 树的深度优先遍历 树的重心
树的一种特殊的图,无环连通图 图还分为有向图,无向图 但是无向图其实也是特殊的有向图 (a指向b,b也指向a,每个连接节点都如此,则是无向图) 那我们只需要讨论有向图 有向图的分类 邻接矩阵 …...
ORA-09925 No space left on device 问题处理全过程记录
本篇文章关键字:linux、oracle、审计、ORA-09925 一、故障现像 朋友找到我说是他们备份软件上报错。 问题比较明显,ORA-09925,看起来就是空间不足导致的 二、问题分析过程 这里说一下逐步的分析思路,有个意外提前说一下就是我…...
Java开发者の模型召唤术:LangChain4j咏唱指南(三)
Java开发者の模型召唤术:LangChain4j咏唱指南(三) 往期回顾: Java开发者の模型召唤术:LangChain4j咏唱指南(一)Java开发者の模型召唤术:LangChain4j咏唱指南(二) 上两期博客中简单的为大家介绍了 langchain4j是什么、java 集成…...
【leetcode100】动态规划Java版本
70. 爬楼梯 题目 思考的时候觉得情况很多,无从下手,卡在了找推导公式这一步。 看了随想录后知道以简单的三个阶梯来推导dp公式,为什么不是四个,五个为一组呢?因为题目要求的只能爬1个阶梯,或者2个阶梯&…...
RSA和ECC在密钥长度相同的情况下哪个更安全?
现在常见的SSL证书,如:iTrustSSL都支持RSA和ECC的加密算法,正常情况下RAS和ECC算法该如何选择呢?实际上在密钥长度相同的情况下,ECC(椭圆曲线密码学)通常比RSA(Rivest-Shamir-Adle…...
YOLO 获取 COCO 指标终极指南 | 从标签转换到 COCOAPI 评估 (训练/验证) 全覆盖【B 站教程详解】
✅ YOLO 轻松获取论文 COCO 指标:AP(small,medium,large )| 从标签转换到 COCOAPI 评估 (训练/验证) 全覆盖 文章目录 一、摘要二、为什么需要 COCO 指标评估 YOLO 模型?三、核心挑战与解决方案 (视频教程核…...
【算法竞赛】dfs+csp综合应用(蓝桥2023A9像素放置)
目录 一、 题目 二、思路 (1)算法框架选择 (2)剪枝策略 具体来说就是: 三、代码 (1) 数据读取与初始化 (2) 检查当前填充是否符合要求 (3) 递归 DFS 进行填充 (4) 读取输入 & 调用 DFS (5) 完整代码 一…...
3D点云配准RPM-Net模型解读(附论文+源码)
RPM-Net 总体流程代码数据预处理模型计算 α α α和 β β β特征提取变换矩阵计算损失 论文链接:RPM-Net: Robust Point Matching using Learned Features 官方链接:RPMNet 老规矩,先看看效果。 看看论文里给的对比图 总体流程 在学…...
23种设计模式-行为型模式-命令
文章目录 简介问题解决代码核心设计优势 总结 简介 命令是一种行为设计模式, 它能把请求转换为一个包含与请求相关的所有信息 的独立对象。这个转换能让你把请求方法参数化、延迟请求执行或把请求放在队列里,并且能实现可撤销操作。 问题 假如你正在开…...
ngx_cpystrn
定义在 src\core\ngx_string.c u_char * ngx_cpystrn(u_char *dst, u_char *src, size_t n) {if (n 0) {return dst;}while (--n) {*dst *src;if (*dst \0) {return dst;}dst;src;}*dst \0;return dst; } ngx_cpystrn 函数的作用是安全地将源字符串(src&#x…...
常用的国内镜像源
常见的 pip 镜像源 阿里云镜像:https://mirrors.aliyun.com/pypi/simple/ 清华大学镜像:https://pypi.tuna.tsinghua.edu.cn/simple 中国科学技术大学镜像:https://pypi.mirrors.ustc.edu.cn/simple/ 豆瓣镜像:https://pypi.doub…...
【小沐杂货铺】基于Three.JS绘制太阳系Solar System(GIS 、WebGL、vue、react)
🍺三维数字地球系列相关文章如下🍺:1【小沐学GIS】基于C绘制三维数字地球Earth(456:OpenGL、glfw、glut)第一期2【小沐学GIS】基于C绘制三维数字地球Earth(456:OpenGL、glfw、glut)第二期3【小沐…...
Navicat17详细安装教程(附最新版本安装包和补丁)2025最详细图文教程安装手册
目录 前言:为什么选择Navicat 17? 一、下载Navicat17安装包 二、安装Navicat 1.运行安装程序 2.启动安装 3.同意“协议” 4.设置安装位置 5.创建桌面图标 6.开始安装 7.安装完成 三、安装补丁 1.解押补丁包 2.在解压后的补丁包目录下找到“w…...
记忆宫殿APP:全方位脑力与思维训练,助你提升记忆力,预防老年痴呆
记忆宫殿APP,一款专业的记忆训练软件,能去帮你提升自己的记忆能力,多样的训练项目创新的记忆方法,全方面帮你去提升你的记忆能力。 记忆宫殿APP有丰富的记忆训练项目,如瞬间记忆、短时记忆、机械记忆等,以…...
SpringBoot+Spring+MyBatis相关知识点
目录 一、相关概念 1.spring框架 2.springcloud 3.SpringBoot项目 4.注解 5.SpringBoot的文件结构 6.启动类原理 二、相关操作 1.Jar方式打包 2.自定义返回的业务状态码 3.Jackson 4.加载配置文件 5.异常处理 三、优化配置 1.简化sql语句 2.查询操作 复杂查询 一…...
【力扣hot100题】(050)岛屿数量
一开始还以为会很难很难(以为暴力搜索会时间超限要用别的办法),没想到并不难。 我最开始是用vector<vector<bool>>记录搜索过的地域,每次递归遍历周围所有地域。 class Solution { public:vector<vector<char…...
Opencv计算机视觉编程攻略-第九节 描述和匹配兴趣点
一般而言,如果一个物体在一幅图像中被检测到关键点,那么同一个物体在其他图像中也会检测到同一个关键点。图像匹配是关键点的常用功能之一,它的作用包括关联同一场景的两幅图像、检测图像中事物的发生地点等等。 1.局部模板匹配 凭单个像素就…...
pat学习笔记
two pointers 双指针 给定一个递增的正整数序列和一个正整数M,求序列中的两个不同位置的数a和b,使得它们的和恰好为M,输出所有满足条件的方案。例如给定序列{1,2,3,4,5,6}和正整数M 8,就存在268和358成立。 容易想到࿱…...