Redis实战篇(一)短信登录

news2024/12/24 22:01:19

Redis实战篇(一)短信登录

1.1、导入黑马点评项目

1.1.1 、导入SQL

3a03d0f4c489ef29439c011ff0601e3f.png

1.1.2、有关当前模型

手机或者app端发起请求,请求我们的nginx服务器,nginx基于七层模型走的事HTTP协议,可以实现基于Lua直接绕开tomcat访问redis,也可以作为静态资源服务器,轻松扛下上万并发, 负载均衡到下游tomcat服务器,打散流量,我们都知道一台4核8G的tomcat,在优化和处理简单业务的加持下,大不了就处理1000左右的并发, 经过nginx的负载均衡分流后,利用集群支撑起整个项目,同时nginx在部署了前端项目后,更是可以做到动静分离,进一步降低tomcat服务的压力,这些功能都得靠nginx起作用,所以nginx是整个项目中重要的一环。

在tomcat支撑起并发流量后,我们如果让tomcat直接去访问Mysql,根据经验Mysql企业级服务器只要上点并发,一般是16或32 核心cpu,32 或64G内存,像企业级mysql加上固态硬盘能够支撑的并发,大概就是4000起~7000左右,上万并发, 瞬间就会让Mysql服务器的cpu,硬盘全部打满,容易崩溃,所以我们在高并发场景下,会选择使用mysql集群,同时为了进一步降低Mysql的压力,同时增加访问的性能,我们也会加入Redis,同时使用Redis集群使得Redis对外提供更好的服务。

8e02610c0f249bf8d3971c03cf769343.png

1.1.3、导入后端项目

在资料中提供了一个项目源码:

9ab52d139f4177902a4a6d8331838da0.png

1.1.4、导入前端工程

c0e3f5a425d54d2bae1352471322447b.png

1.1.5 运行前端项目

7f58d7c64c823e22f572e264bb64480c.png

1.2 、基于Session实现登录流程

发送验证码:

用户在提交手机号后,会校验手机号是否合法,如果不合法,则要求用户重新输入手机号

如果手机号合法,后台此时生成对应的验证码,同时将验证码进行保存,然后再通过短信的方式将验证码发送给用户

短信验证码登录、注册:

用户将验证码和手机号进行输入,后台从session中拿到当前验证码,然后和用户输入的验证码进行校验,如果不一致,则无法通过校验,如果一致,则后台根据手机号查询用户,如果用户不存在,则为用户创建账号信息,保存到数据库,无论是否存在,都会将用户信息保存到session中,方便后续获得当前登录信息

校验登录状态:

用户在请求时候,会从cookie中携带者JsessionId到后台,后台通过JsessionId从session中拿到用户信息,如果没有session信息,则进行拦截,如果有session信息,则将用户信息保存到threadLocal中,并且放行

补充: 为什么要使用ThreadLocal?

ThreadLoal是线程独享的,每个进入Tomcat的请求都对应着一个线程,将来ThreadLocal会在线程内开辟一片空间来保存用户信息,这些线程互不干扰.。这样的话,不同的用户访问相同的Controller都有自己的线程,也就会有自己的ThreadLocal信息,大家互不干扰。

如何进行退出呢?

在拦截器的最后一个方法中,清空thread local的信息,第一可以做到退出登录,第二可以防止内存泄露

5ade3f3d142b175a3b7ba1e82e18b84b.png

1.3 、实现发送短信验证码功能

页面流程

11dc596a3ef11caa9ece1720a744c4fb.png

问题:为啥后端接口是8081,但是请求确实8080呢?

这里为了解决跨域问题请求位于8080的Nginx服务,Nginx的代理再去请求8081Tomcat服务器

具体代码如下

贴心小提示:

具体逻辑上文已经分析,我们仅仅只需要按照提示的逻辑写出代码即可。

发送验证码

@Override
public Result sendCode(String phone, HttpSession session) {
    // 1.校验手机号
    if(RegexUtils.isPhoneInvalid(phone)){
        // 2.如果不符合,返回错误信息
        return Result.fail("手机号格式错误");
    }
    // 3.如果符合,生成验证码
    String code = RandomUtil.randomNumbers(6);

    // 4.保存验证码到session
    session.setAttribute("code"+phone,code);
    // 5.发送验证码
    String[] phoneNumber = new String[1];
    String[] templateParam = new String[2];
    phoneNumber[0] = phone;
    templateParam[0] = code;
    templateParam[1] = "5";
    SendSmsUtil.sendSms(phoneNumber,templateParam);
    return Result.ok();
}
  • 登录
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1.验证手机号是否正确
    String code = loginForm.getCode();
    String phone = loginForm.getPhone();
    if(RegexUtils.isPhoneInvalid(phone)){
        return Result.fail("手机号格式错误!");
    }
    // 2.查询手机号对应的验证码是否一致(存在)
    String cacheCode = (String) session.getAttribute("code" + loginForm.getPhone());
    if(cacheCode != null && !cacheCode.equals(code)){
        // 3.不一致,报错
        return Result.fail("验证码错误!");
    }

    // 4.一致,根据手机号查询对应的用户
    User user = this.query().eq("phone", phone).one();
    // 5.判断用户是否存在
    if(user == null){
        // 6.不存在,则创建新用户,并保存到数据库
        user  = createUserWithPhone(phone);
    }
    // 7.存在,则保存用户到session
    session.setAttribute("user",user);
    return Result.ok();
}

注意:这里是否需要返回登录成功的凭证信息呢?

不需要,因为登录或注册后会在session中存放user,一个session对应一个sessionID,sessionID会被自动放到Cookie,下次请求时,Cookie会带着JSessionID找到对应的session

1.4、实现登录拦截功能

温馨小贴士:tomcat的运行原理

239895787e4fc1fc1a5f845ae8c76868.png

当用户发起请求时,会访问我们像tomcat注册的端口,任何程序想要运行,都需要有一个线程对当前端口号进行监听,tomcat也不例外,当监听线程知道用户想要和tomcat连接连接时,那会由监听线程创建socket连接,socket都是成对出现的,用户通过socket像互相传递数据,当tomcat端的socket接受到数据后,此时监听线程会从tomcat的线程池中取出一个线程执行用户请求,在我们的服务部署到tomcat后,线程会找到用户想要访问的工程,然后用这个线程转发到工程中的controller,service,dao中,并且访问对应的DB,在用户执行完请求后,再统一返回,再找到tomcat端的socket,再将数据写回到用户端的socket,完成请求和响应

通过以上讲解,我们可以得知 每个用户其实对应都是去找tomcat线程池中的一个线程来完成工作的, 使用完成后再进行回收,既然每个请求都是独立的,所以在每个用户去访问我们的工程时,我们可以使用threadlocal来做到线程隔离,每个线程操作自己的一份数据

温馨小贴士:关于threadlocal

如果小伙伴们看过threadLocal的源码,你会发现在threadLocal中,无论是他的put方法和他的get方法, 都是先从获得当前用户的线程,然后从线程中取出线程的成员变量map,只要线程不一样,map就不一样,所以可以通过这种方式来做到线程隔离

为什么要使用拦截器呢?

如图:如果没有拦截器,那么对于每个Controller都需要一个校验用户是否登录的逻辑代码,代码冗余较大。所有的请求到Controller之前可以被拦截器或者过滤器 拦截,这样的话只要我们在拦截器这一步进行校验就不用再进入Controller了,大大减少了性能损耗!

57a72f0aa0493434e3224b3d5fcb6c6a.png

60cc08ab46dc0f560dc86cef719df43b.png

拦截器代码

/**
 * @author lxy
 * @version 1.0
 * @Description 登录拦截器
 * @date 2022/11/24 1:12
 */
public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 在进入Controller之前会被执行
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取session
        HttpSession session = request.getSession();
        // 2.获取session中的用户
        Object user = session.getAttribute("user");
        // 3.如果用户不存在,则拦截,返回401状态码
        if(user == null){
            response.setStatus(401);
            return false;
        }

        // 4.如果存在,则保存到ThreadLocal
        UserHolder.saveUser((User) user);
        // 5.放行
        return true;
    }

    /**
     * 在执行完Controller里面的逻辑后执行下面代码
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}

让拦截器生效

/**
 * @author lxy
 * @version 1.0
 * @Description MVC配置类
 * @date 2022/11/24 1:22
 */
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 增加登录拦截器,并对不必要的请求路径排除拦截
        registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(
                "/user/login",
                "/user/code",
                "/blog/hot",
                "/shop/**",
                "/shop-type/**",
                "/upload/**",
                "/voucher/**"
        );
    }
}

1.5、隐藏用户敏感信息

我们通过浏览器观察到此时用户的全部信息都在,这样极为不靠谱,所以我们应当在返回用户信息之前,将用户的敏感信息进行隐藏,采用的核心思路就是书写一个UserDto对象,这个UserDto对象就没有敏感信息了,我们在返回前,将有用户敏感信息的User对象转化成没有敏感信息的UserDto对象,那么就能够避免这个尴尬的问题了

在登录方法处修改

// 7.存在,则保存用户到session
session.setAttribute("user", BeanUtil.copyProperties(user,UserDTO.class));

在拦截器处:

// 4.如果存在,则保存到ThreadLocal
UserHolder.saveUser((UserDTO) user);

在UserHolder处:将user对象换成UserDTO

public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    public static UserDTO getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}

前端登录结果:

a4d3ad187ed6924823b4ee6e1ad110df.png

1.6、session共享问题

核心思路分析:

每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session,所以此时 整个登录拦截功能就会出现问题,我们能如何解决这个问题呢?早期的方案是session拷贝,就是说虽然每个tomcat上都有不同的session,但是每当任意一台服务器的session修改时,都会同步给其他的Tomcat服务器的session,这样的话,就可以实现session的共享了

但是这种方案具有两个大问题

1、每台服务器中都有完整的一份session数据,服务器压力过大。

2、session拷贝数据时,可能会出现延迟

所以咱们后来采用的方案都是基于redis来完成,我们把session换成redis,redis数据本身就是共享的,就可以避免session共享的问题了

5af3edf08f28172fba5e4c8d7a224970.png

1.7 Redis代替session的业务流程

1.7.1、设计key的结构

首先我们要思考一下利用redis来存储数据,那么到底使用哪种结构呢?由于存入的数据比较简单,我们可以考虑使用String,或者是使用哈希,如下图。

如果使用String,注意他的value,会多占用一点空间(比如标点,括号等),而且字段修改,只能对Value整体修改,然后再设置进去

如果使用哈希,则他的value中只会存储数据本身,而且可以做到精确的修改Value中的某个字段(name,age)

如果不是特别在意内存,其实使用String就可以啦。

73ed6c4d0daea880f18d24c102bbf8b5.png

1.7.2、设计key的具体细节

所以我们可以使用String结构,就是一个简单的key,value键值对的方式,但是关于key的处理,session他是每个用户都有自己的session(因为是在ThreadLocal中的),但是redis的key是共享的,咱们就不能使用code了

在设计这个key的时候,我们之前讲过需要满足两点

1、key要具有唯一性

2、key要方便携带

如果我们采用phone:手机号这个的数据来存储当然是可以的,但是如果把这样的敏感数据存储到redis中并且从页面中带过来毕竟不太合适(信息容易泄露),所以我们在后台生成一个随机串token,然后让前端带来这个token就能完成我们的整体逻辑了

1.7.3、整体访问流程

当注册完成后,用户去登录会去校验用户提交的手机号和验证码,是否一致,如果一致,则根据手机号查询用户信息,不存在则新建,最后将用户数据保存到redis,并且生成token作为redis的key,当我们校验用户是否登录时,会去携带着token进行访问,从redis中取出token对应的value,判断是否存在这个数据,如果没有则拦截,如果存在则将其保存到threadLocal中,并且放行。

6663f5dfbcd117a46a6c5a946c00dcb3.png

session和Redis 验证用户登录原理的区别

之前我们使用session进行登录的时候,当把User存到session后,会自动把SessionID存到Cookie中,下次发送请求的时候会携带着。通过SessionId便可知道用户是否登录。

当我们使用Redis后,key == 》token:随机字符串,value是User。这时 为了让用户下次发送请求携带着key,就需要前端特殊处理了~

675047c7705e2439ec51237a3a7db215.png

bd30ae4e630682506b78cfb4a9d020cf.png

1.8 基于Redis实现短信登录

这里具体逻辑就不分析了,之前咱们已经重点分析过这个逻辑啦。

UserServiceImpl代码

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1.验证手机号是否正确
    String code = loginForm.getCode();
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        return Result.fail("手机号格式错误!");
    }
    // 2.查询手机号对应的验证码是否一致(存在)
    String cacheCode = (String) stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
    if (cacheCode != null && !cacheCode.equals(code)) {
        // 3.不一致,报错
        return Result.fail("验证码错误!");
    }

    // 4.一致,根据手机号查询对应的用户
    User user = this.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 准备用户基本信息
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    //注意:这里如果直接这样写 BeanUtil.beanToMap(userDTO) 会报错: Long cannot be cast to String。
    //     因为我们使用的是stringRedisTemplate,里面存的map的key和value都必须是String.
    //解决办法:1.不使用该方法,而是手动把userDTO放入Map<String,String>,该转换的手动转
    //        2.自定义value的转换规则,可以通过 BeanUtil.beanToMap(obj,map,copyOptions)
    Map <String, Object> map = BeanUtil.beanToMap(userDTO, new HashMap <>(), CopyOptions.create()
                                                  .setIgnoreNullValue(true)
                                                  .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
    // 7.3 保存到Redis  (这里也可使用put(),但是Value中的key和value是分开放的,需要多次和数据库交互~)
    String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
    stringRedisTemplate.opsForHash().putAll(tokenKey, map);
    // 7.4 设置token有效期
    stringRedisTemplate.expire(tokenKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

    // 8.把token返回给前端
    return Result.ok(token);
}

LoginInterceptor代码

/**
 * @author lxy
 * @version 1.0
 * @Description 登录拦截器
 * @date 2022/11/24 1:12
 */
public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 因为LoginInterceptor是new 出来的,并没有交给Spring容器管理,所以我们不能使用Autowire或者@Resource注入,
     * 这里可以使用构造函数,在使用到拦截器的时候,传入需要的对象
     */
    private StringRedisTemplate stringRedisTemplate;

    public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 在进入Controller之前会被执行
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            // 不存在
            response.setStatus(401);
            return false;
        }

        // 2.根据token获取用户信息
        Map <Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY+token);
        if(userMap==null){
            response.setStatus(401);
            return false;
        }

        // 3.如果存在,则保存到ThreadLocal
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        UserHolder.saveUser(userDTO);

        // 4.刷新用户token的有效时间 (只要用户在这段时间内用户在线,那么就不会过期)
        String tokenKey = RedisConstants.LOGIN_USER_KEY+token;
        stringRedisTemplate.expire(tokenKey,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 5.放行
        return true;
    }

    /**
     * 在执行完Controller里面的逻辑后执行下面代码
     */    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}

1.9 解决状态登录刷新问题

1.9.1 初始方案思路总结:

在这个方案中,他确实可以使用对应路径的拦截,同时刷新登录token令牌的存活时间,但是现在这个拦截器他只是拦截需要被拦截的路径,假设当前用户访问了一些不需要拦截的路径(比如首页),那么这个拦截器就不会生效,所以此时令牌刷新的动作实际上就不会执行,所以这个方案他是存在问题的

7499024a5e38f91d234cc337eb4a3e7b.png

1.9.2 优化方案

既然之前的拦截器无法对不需要拦截的路径生效,那么我们可以添加一个拦截器,在第一个拦截器中拦截所有的路径,把第二个拦截器做的事情放入到第一个拦截器中,同时刷新令牌(token)。因为第一个拦截器有了threadLocal的数据,所以此时第二个拦截器只需要判断拦截器中的user对象是否存在即可,完成整体拦截(这次拦截是对那些需要登录的资源)功能。

bbbc7e526413d28f61c5abd562245477.png

1.9.3 代码

RefreshTokenInterceptor

/**
 * @author lxy
 * @version 1.0
 * @Description 登录拦截器
 * @date 2022/11/24 1:12
 */
public class RefreshTokenInterceptor implements HandlerInterceptor {

    /**
     * 因为RefreshTokenInterceptor是new 出来的,并没有交给Spring容器管理,所以我们不能使用Autowire或者@Resource注入,
     * 这里可以使用构造函数,在使用到拦截器的时候,传入需要的对象
     */
    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 在进入Controller之前会被执行
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            // 此处return true是对的,若return false,第一次访问登录页面时就会被拦截;
            // 若return true,第一次访问登录页会进入Login拦截器,由于登录页为放行路径,放行~
            return true;
        }

        // 2.根据token获取用户信息
        Map <Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY+token);
        if(userMap==null){
            return true;
        }

        // 3.如果存在,则保存到ThreadLocal
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        UserHolder.saveUser(userDTO);

        // 4.刷新用户token的有效时间 (只要用户在这段时间内用户在线,那么就不会过期)
        String tokenKey = RedisConstants.LOGIN_USER_KEY+token;
        stringRedisTemplate.expire(tokenKey,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 5.放行
        return true;
    }

    /**
     * 在执行完Controller里面的逻辑后执行下面代码
     */    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}

LoginInterceptor

/**
 * @author lxy
 * @version 1.0
 * @Description 登录拦截器
 * @date 2022/11/24 1:12
 */
public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 在进入Controller之前会被执行
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 如果没有登录则拦截
        if(UserHolder.getUser()==null){
            response.setStatus(401);
            return false;
        }
        // 用户已经登录则放行
        return true;
    }
}

总结:Redis代替Session需要考虑的问题

选择合适的数据结构

比如为什么code选择用string,token选择用Hash

选择合适的Key

比如为什么验证码key是phone, token的key是一个随机字符串

选择合适的存储力度

比如我们存储的是UserDTO,相对于User把其中敏感的信息去掉,也可以节约内存空间

选择合适的有效期

写在最后

如果这篇【文章】有帮助到你,希望可以给【JavaGPT】点个赞👍,创作不易,如果有对【后端技术】、【前端领域】感兴趣的小可爱,也欢迎关注❤️❤️❤️ 【JavaGPT】❤️❤️❤️,我将会给你带来巨大的【收获与惊喜】💝💝💝!

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

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

相关文章

《尚品甄选》:后台系统——结合redis实现用户登录

文章目录 一、统一结果实体类二、统一异常处理三、登录功能实现四、CORS解决跨域五、图片验证码六、登录校验功能实现6.1 拦截器开发6.2 拦截器注册 七、ThreadLocal 要求&#xff1a; 用户输入正确的用户名、密码以及验证码&#xff0c;点击登录可以跳转到后台界面。未登录的用…

6、独立按键控制LED亮灭

独立按键 轻触按键&#xff1a;相当于是一种电子开关&#xff0c;按下时开关接通&#xff0c;松开是开关断开 实现原理&#xff1a;是通过轻触按键内部的金属弹片受力弹动来实现接通和断开 代码&#xff1a; #include <REGX52.H>void main() {//等同于P20XFE;P2_00…

张弛声音变现课,枪战电影高能量、快速节奏

在执行枪战片的声音配音任务时&#xff0c;配音员应该致力于传递出戏剧性的紧张氛围与动作场面的激烈感。枪战场景往往是高能量、快速节奏的&#xff0c;这就要求配音不仅要与视觉动作紧密结合&#xff0c;还要通过声音来增强动作的逼真度和观众的紧迫感。以下是针对枪战电影进…

IT行业多项目管理的方法与策略:优化资源分配与提升项目成功率

多项目管理已成为项目经理们面临的常态&#xff0c;IT行业如何高效进行项目管理呢&#xff1f; 多项目管理过程中存在的问题 1、多类型项目并行&#xff0c;项目流程掺杂混乱&#xff0c;项目进度难以监控&#xff0c;反应缓慢&#xff0c;容易产生延误风险。 2、团队资源有…

LeetCode-1689. 十-二进制数的最少数目 C/C++实现 超详细思路及过程[M]

&#x1f388;归属专栏&#xff1a;深夜咖啡配算法 &#x1f697;个人主页&#xff1a;Jammingpro &#x1f41f;记录一句&#xff1a;上一篇博客这里好像没改&#xff0c;那就不改了。 文章目录 LeetCode-1689. 十-二进制数的最少数目&#x1f697;题目&#x1f686;题目描述&…

2016年11月16日 Go生态洞察:Go字体的创新之旅

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

SpringBoot修改启动图标

公司基于SpringBoot框架再次定制了一个框架,那要怎么让自己封装的框架逼格高一点? 那门面就要与众不同, 哈哈哈!!!! 艺术文字网站: patorjk.com 我这里使用字体是 "怪物" 貌似框架也是使用这个字体,外国人的文化底蕴就是xxxx,推崇妖魔鬼怪

WorldWind Android上加载白模数据

这篇文章介绍下如何加载白模数据。这个白模数据的格式是shapefile格式的文件。白模数据拷贝到手机本地&#xff0c;然后读取白模数据&#xff0c;进行加载展示。 worldwind android本身是不支持加载白模数据的&#xff0c;但是可以根据现有提供的加载Polygons的方式&#xff0c…

自定义精美商品分类列表组件 侧边栏商品分类组件 category组件(适配vue3)

随着技术的发展&#xff0c;开发的复杂度也越来越高&#xff0c;传统开发方式将一个系统做成了整块应用&#xff0c;经常出现的情况就是一个小小的改动或者一个小功能的增加可能会引起整体逻辑的修改&#xff0c;造成牵一发而动全身。通过组件化开发&#xff0c;可以有效实现单…

【Linux】 sudo命令使用

sudo sudo是linux系统管理指令&#xff0c;是允许系统管理员让普通用户执行一些或者全部的root命令的一个工具&#xff0c;如halt&#xff0c;reboot&#xff0c;su等等。这样不仅减少了root用户的登录 和管理时间&#xff0c;同样也提高了安全性。sudo不是对shell的一个代替…

Spring cloud - Feign

Feign的作用 Feign是Netflix公司开发的声明式web客户端组件&#xff0c;Spring对Feign做了无缝集成&#xff1a; Feign is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has plugg…

【TC3xx芯片】TC3xx芯片的Endinit功能详解

目录 前言 正文 1.功能概述 2. WDTxCON0 的密码访问&#xff08;Password Access to WDTxCON0&#xff09; 2.1 Static Password 2.2 Automatic Password Sequencing 2.3 Time-Independent Pasword 2.4 Time Check Password 3. WDTxCON0的检查访问&#xff08;Check A…

【C++初阶】STL详解(八)List的模拟实现

本专栏内容为&#xff1a;C学习专栏&#xff0c;分为初阶和进阶两部分。 通过本专栏的深入学习&#xff0c;你可以了解并掌握C。 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn ⏩专栏分类&#xff1a;C &#x1f69a;代码仓库&#xff1a;小小unicorn的代码仓库&…

JOSEF约瑟 JHOK-ZBZ201智能型漏电(剩余)继电器 导轨安装

JHOK-ZBZ漏电继电器&#xff08;以下简称继电器&#xff09;适用于交流电压至660V或更高的TN、TT、和IT系统&#xff0c;频率为50Hz。通过零序电流互感器检测出超过整定值的零序&#xff08;剩余&#xff09;漏电电流。该继电器与分励脱扣器或失压脱扣器的断路器、交流接触器、…

激活函数与其导数:神经网络中的关键元素

激活函数是神经网络中的重要组成部分&#xff0c;有力地推动了深度学习的发展。然而&#xff0c;仅仅了解和选择激活函数是不够的&#xff0c;我们还需要理解激活函数的导数。本文将详细介绍激活函数的概念、作用及其导数的重要性&#xff0c;并探究导数对神经网络训练的影响。…

2016年12月13日 Go生态洞察:2016年Go用户调查与企业问卷

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

【UGUI】中Content Size Fitter)组件-使 UI 元素适应其内容的大小

官方文档&#xff1a;使 UI 元素适应其内容的大小 - Unity 手册 必备组件&#xff1a;Content Size Fitter 通常&#xff0c;在使用矩形变换定位 UI 元素时&#xff0c;应手动指定其位置和大小&#xff08;可选择性地包括使用父矩形变换进行拉伸的行为&#xff09;。 但是&a…

如何减少40%的Docker构建时间

随着Docker的普及&#xff0c;许多公司的产品会将组件构建为Docker镜像。但随着时间的推移&#xff0c;一些镜像变得越来越大&#xff0c;对应的CI构建也变得越来越慢。 如果能在喝完一杯咖啡的时间&#xff08;不超过5分钟&#xff09;内完成构建&#xff0c;将是一个理想状态…

使用Kibana让es集群形象起来

部署Elasticsearch集群详细步骤参考本人&#xff1a; https://blog.csdn.net/m0_59933574/article/details/134605073?spm1001.2014.3001.5502https://blog.csdn.net/m0_59933574/article/details/134605073?spm1001.2014.3001.5502 kibana部署 es集群设备 安装软件主机名…

【数据库】物理操作的一趟扫描算法机制原理,理解关系代数据与物理计划的关系,以及代价评估的应用和算法优化

一趟扫描算法 ​专栏内容&#xff1a; 手写数据库toadb 本专栏主要介绍如何从零开发&#xff0c;开发的步骤&#xff0c;以及开发过程中的涉及的原理&#xff0c;遇到的问题等&#xff0c;让大家能跟上并且可以一起开发&#xff0c;让每个需要的人成为参与者。 本专栏会定期更新…