从零开始学习秒杀项目

news2024/11/15 19:38:46

        构思了很多种讲述这个简易版的秒杀项目的思路,比如按照功能分类,按照项目亮点串起来讲述,总觉得不适合基础薄弱的同学来学习,所以本项目按照从搭建开始,过程中需要什么来学习什么。

技术栈

SpringBoot+mybatisPlus,MySQL,Redis,RabbitMQ

项目地址

ma/seckill

亮点

1.自定义注解(Spring AOP)

2.用户密码两次MD5加密

        第一次MD5加密:防止用户明文密码在网络进行传输

        第二次MD5加密:防止数据库被盗,避免通过MD5反推出密码,双重保险

3.redis分布式锁保证秒杀业务场景下的正确性

4.秒杀操作后的秒杀成功信息进入RabbitMQ进行排队

 项目代码结构图

准备

创建SpringBoot项目,写入Maven文件,导入sql文件。

数据库介绍

使用MYSQL

goods代表货物信息表

order_info订单详情表

seckill_good秒杀商品表

seckill_order秒杀商品订单表

user用户表

Redis服务启动

这里使用的是Redis-windows版本,无密码

RabbitMQ服务启动

如果你不会安装RabbitMQ,请查看windows环境下安装RabbitMQ(超详细)_windows安装rabbitmq-CSDN博客

http://localhost:15672/#/保证本机RabbitMQ服务启动(账号是默认账号guest/guest)

http://localhost:15672/#/

Coding

用户登录模块

对应LoginController.java

一共有两个方法,一个是界面跳转方法,略过

    @RequestMapping("/do_login")
    @ResponseBody
    public Result<String> doLogin(@Valid LoginParam loginParam, HttpServletResponse response){
        System.out.println(loginParam);
        //登陆
        String token = userService.login(response, loginParam);
        return Result.success(token);
    }

首先来说一下整个方法的返回值Result.java

public class Result<T> {
    private int code;
    private String msg;
    private T data;

    private Result(T data) {
        this.code = CodeMsg.SUCCESS.getCode();
        this.msg = CodeMsg.SUCCESS.getMsg();
        this.data = data;
    }
    public boolean isSuccess(){
        return this.code==CodeMsg.SUCCESS.getCode();
    }
    public static <T> Result<T> success(T data){
        return new Result<T>(data);
    }
    public static <T> Result<T> error(CodeMsg codeMsg){
        return new Result<T>(codeMsg);
    }
    private Result(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    private Result(CodeMsg codeMsg) {
        if(codeMsg != null) {
            this.code = codeMsg.getCode();
            this.msg = codeMsg.getMsg();
        }
    }
    public int getCode() {
        return code;
    }
    public void setCode(int code) {
        this.code = code;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
}

里面有三个变量,分别代表着结果代码,结果信息,结果中返回的数据,T代表泛型的意思,不懂的可以百度;CodeMsg代表的具体定义的一些成功和异常信息。

接下来我们再返回LoginController里面的doLogin方法,可以注意到在函数参数中使用了@Valid注解,代表着LoginParam需要进行参数检查,我们进入LoginParam.java

import com.lgc.SeckillProject.vaildator.IsMobile;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.NotNull;

@Getter
@Setter
@ToString
public class LoginParam {

    @NotNull(message = "手机号不能为空")
    @IsMobile
    private String mobile;

    @NotNull
    @Length(min = 23,message = "密码长度需要在7个字以内")
    private String password;
}

上面的代码特意粘贴了使用注解来源于哪个包,明确一下注解的来源。

代码中的两个字段mobile和password两个字段分别用@NotNull注解修饰,意思是两个字段传入的时候不允许为空。

另外password对字段的长度进行了限制.

拓展:

我们还注意到他使用了@IsMobile注解,这是一个自定义的注解,也是本项目中的一个亮点。

我们需要自定义注解,首先创建一个自定义注解类

@Target({METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER}) //注解的作用范围 :方法,字段、枚举的常量,注解,构造函数,方法参数
@Retention(RetentionPolicy.RUNTIME)   //注解的生命周期:默认是class,RUNTIME:运行时存在。RUNTIME>class>source
@Documented   //如果一个注解@B,被@Documented标注,那么被@B修饰的类,生成文档时,会显示@B。如果@B没有被@Documented标准,最终生成的文档中就不会显示@B。
@Constraint(validatedBy = {IsMobileValidator.class})//自定义约束
public @interface IsMobile {
    boolean required() default true;

    String message() default "手机号码格式错误";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}

从上面看,有很多陌生的注解,我们一个一个来分析

首先是@Target注解,里面写入了这个注解的使用范围,包括可以在方法上使用,字段、枚举的常量,注解,构造函数,方法参数

@Retention(RetentionPolicy.RUNTIME)注解的生命周期,一般是RUNTIME,默认是class;RUNTIME>CLASS>SOURCE

@Document .如果一个注解@B,被@Documented标注,那么被@B修饰的类,生成文档时,会显示@B。如果@B没有被@Documented标准,最终生成的文档中就不会显示@B

@Constraint 自定义约束,IsMobileValidator.class是自定义的约束类

自定义注解中有几个方法,并且每个方法中都有默认的值。

接下来我们来看一下注解中自定义约束类IsMobileValidator.java

public class IsMobileValidator implements ConstraintValidator<IsMobile,String> {
    private boolean required=false;



    @Override
    public void initialize(IsMobile constraintAnnotation) {
        required=constraintAnnotation.required();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (required){
            return VaildatorUtil.isMobile(value);
        }else{
            if (StringUtils.isEmpty(value)){
                return true;
            }else{
                return VaildatorUtil.isMobile(value);
            }
        }
    }
}

首先实现接口ConstraintValidator,参数为自定义注解和String

重写里面的initialize方法还有isValid方法,require代表的是需不需要进行验证,isValid是验证方法,如果是需要的验证的话,调用isValid方法,isValid里面又使用ValidatorUtil.isMobild方法,这个方法的内部是使用正则表达式进行实现的。

至此,自定义注解讲解完成。

我们返回LoginController继续,我们来看一下userService.login这个方法,进入UserServiceImpl中login方法。

我们来分析一下上面的红框中的代码,为了防止从浏览器中输入的密码在网络中明文传输,我们使用了MD5进行加密,在从浏览器的输入中获取密码后加入“盐”使用MD5.formPassToDBPass进行加密处理,然后再与数据库中已加密的密码进行比较。

以上是加密的细节。

返回UserServiceImpl,继续看login方法后半段生成cookie部分

使用UUIDUtil工具类生成随机的token ,进入addCookie方法

//生成cookie
String token = UUIDUtil.uuid();
addCookie(response,token,user);
return token;

在Cookie方法中,把生成的token作为key的后半段,UserKey.token作为前半段,拼接构成key值,user对象作为value值传入Redis,并且生成一个Cookie对象放入response中。

private void addCookie(HttpServletResponse response,String token,User user){
        redisService.set(UserKey.token,token,user,UserKey.TOKEN_EXPIRE);
        Cookie cookie = new Cookie(COOKI_NAME_TOKEN, token);
        cookie.setMaxAge(UserKey.TOKEN_EXPIRE);
        cookie.setPath("/");
        response.addCookie(cookie);
    }

这里重点讲解一下redisService.set方法的内部逻辑

/**
     * 设置对象
     *
     * @param prefix 对象Prefix
     * @param key    键
     * @param value  值
     * @param exTime 过期时间
     * @param <T>    返回类型
     * @return
     */
    public <T> boolean set(KeyPrefix prefix,String key,T value,int exTime){
        String str = beanToString(value);
        if (str==null || str.length() <= 0){
            return false;
        }
        //生成唯一key
        String realKey = prefix.getPrefix() + key;
        //设置过期时间
        if (exTime<=0){
            stringRedisTemplate.opsForValue().set(realKey,str);
        }else{
            return stringRedisTemplate.opsForValue().setIfAbsent(realKey,str,exTime, TimeUnit.SECONDS);
        }
        return true;
    }

为了保证整个秒杀业务中商品数量的正确性,关键的一个步骤是并发场景下,锁的竞争,在这里使用了redis的分布式锁机制,也就是setIfAbsent这个方法,我认为也是整个项目的关键之一。在本文中的最后部分《redis分布式锁解析》详细介绍一下这个东西。

订单模块

对应orderController.java

订单模块的控制层只包含一个方法,info 方法,参数为user以及订单号,返回值为订单详情。

该模块大多数为增删改查以及简单的业务逻辑,较为简单,自己顺着代码看看就可以

货物模块

对应GoodsController.java

首先针对list方法进行分析。

为了应对在SpringBoot中的高并发及优化访问速度,我们一般会把页面上的数据查询出来,然后放到redis中进行缓存,减少数据库的压力。如果再进行改进的话,可以对整个界面进行缓存。

@RequestMapping("/list")
    @ResponseBody
    public String list(Model model, HttpServletRequest request, HttpServletResponse response){
        //取缓存
        String html = redisService.get(GoodsKey.getGoodsList, "", String.class);
        if (!StringUtils.isEmpty(html)){
            return html;
        }
        //获取数据绑定到model
        List<GoodsVo> goodsVos = goodService.listGoodVo();
        model.addAttribute("goodsVos",goodsVos);
        WebContext ctx = new WebContext(request, response, request.getServletContext(), request.getLocale(), model.asMap());

        //手动渲染
        html = thymeleafViewResolver.getTemplateEngine().process("goods_list", ctx);
        if (!StringUtils.isEmpty(html)){
            redisService.set(GoodsKey.getGoodsList,"",html,60);
        }
    return html;
    }

goodsDetail和detailStatic思路也是一样的,无非就是添加了秒杀状态还有倒计时这俩,思路和上面的代码是一致的。自己顺着代码理一遍就可以

秒杀模块

对应模块SeckillController.java

实现了InitializingBean接口,需要重写afterPropertiesSet方法,系统启动后,进行初始化,将热点数据放入redis中。

这里在方法里还定义了一个map,我们知道map的存储位置是内存中,所以我们将秒杀的商品的Id放入内存中,查询速度会飞快。

下面介绍一下秒杀接口1.0以及改进版本,从一开始的QPS793经过优化后QPS1658

首先是1.0版本    QPS:793 * 线程:5000 * 10 * 进行秒杀

@RequestMapping("/do_seckill")
    public String seckill(Model model, User user, @RequestParam("goodsId")long goodsId){
        model.addAttribute("user",user);
        if (user==null){
            return "login";
        }
        //库存判断
        GoodsVo goodsVo = goodsService.getGoodsVoById(goodsId);
        int stock = goodsVo.getStockCount();
        if (stock<=0){
            model.addAttribute("ErrorMsg", CodeMsg.MIAO_SHA_OVER.getMsg());
            return "seckill_fail";
        }
        //判断是否秒杀到了
        SeckillOrder order = orderService.getOrderByUserIdGoodsId(user.getId(), goodsId);
        if (order!=null){
            model.addAttribute("ErrorMsg",CodeMsg.REPEATE_MIAOSHA.getMsg());
            return "seckill_fail";
        }
        //减库存下订单,写入秒杀订单
        OrderInfo orderInfo = seckillService.seckill(user, goodsVo);
        model.addAttribute("orderInfo",orderInfo);
        model.addAttribute("goods",goodsVo);
        return "order_detail";

    }

核心是将处理好的数据放到model,界面查询速度偏慢

2.0版本    * QPS:1206 * 线程:5000 * 10 * 订单页面静态化

@RequestMapping(value = "/seckill",method = RequestMethod.POST)
    @ResponseBody
    public Result<OrderInfo> seckillStatic(User user,@RequestParam("goodsId")long goodsId){
        if (user==null){
            return Result.error(CodeMsg.SESSION_ERROR);
        }
        //判断库存
        GoodsVo goods = goodsService.getGoodsVoById(goodsId);
        int stock=goods.getStockCount();
        if (stock<=0){
            return Result.error(CodeMsg.MIAO_SHA_OVER);
        }
        //判断是否已经秒杀到了
        SeckillOrder order = orderService.getOrderByUserIdGoodsId(user.getId(), goodsId);
        if (order!=null){
            return Result.error(CodeMsg.REPEATE_MIAOSHA);
        }
        //减少库存,写入秒杀订单
        OrderInfo orderInfo = seckillService.seckill(user, goods);
        return Result.success(orderInfo);
    }

整体的流程和1.0没有区别,区别在于将最后的结果封装在Result中了。速度还可以再提升

3.0版 * QPS:1658 * 线程:5000 * 10 * 加入消息队列

@RequestMapping(value = "/{path}/seckill_mq",method = RequestMethod.POST)
    @ResponseBody
    public Result<Integer> seckillMq(User user, @RequestParam("goodsId")long goodsId, @PathVariable("path")String path){
        if (user==null){
            return Result.error(CodeMsg.SESSION_ERROR);
        }
        //验证path
        boolean checkPath = seckillService.checkPath(user, goodsId, path);
        if (!checkPath){
            return Result.error(CodeMsg.REQUEST_ILLEGAL);
        }
        //内存标记,减少Redis访问
        Boolean over = localOverMap.get(goodsId);
        if (over){
            return Result.error(CodeMsg.MIAO_SHA_OVER);
        }
        //预减库存
        Long stock = redisService.decr(GoodsKey.getSeckillGoodStock, "" + goodsId);
        if (stock<0){
            localOverMap.put(goodsId,true);
            return Result.error(CodeMsg.MIAO_SHA_OVER);
        }
        //判断是否秒杀到了
        SeckillOrder order = orderService.getOrderByUserIdGoodsId(user.getId(), goodsId);
        if (order!=null){
            return Result.error(CodeMsg.REPEATE_MIAOSHA);
        }
        //压入消息队列中
        //入队
        SeckillMessage sm = new SeckillMessage();
        sm.setUser(user);
        sm.setGoodsId(goodsId);
        sender.sendSeckillMessage(sm);
        return Result.success(0);//排队中
    }

3.0版本一个是引入了本地map减少了Redis的访问,另外使用了预减库存,秒杀成功后将秒杀成功信息发送到RabbitMQ中,进入队列中排队。

从代码中可以看出预减成功后也只是进入到了RabbitMQ中进行排队,不至于阻塞,至于最后入库,还需要对队列进行监听。

值得说一点的是,秒杀接口操作的层次仅仅只在Redis中,所有操作的数据都在Redis中,所以此过程不存在与数据库的任何操作,也就是说你如果在秒杀过程中失败了,不会影响到数据库中的数据,这是极为巧妙的,也是值得学习的,只有当秒杀成功后,秒杀成功的消息放入MQ,并且MQ监听到的时候,此时监听到的信息才会真正的创建订单并存入数据库。

 @RabbitListener(queues = MQConfig.SECKILL_QUEUE)
    public void receive(String message){
        log.info("receive message:"+message);
        SeckillMessage sm = redisService.stringToBean(message, SeckillMessage.class);
        User user = sm.getUser();
        long goodsId = sm.getGoodsId();

        GoodsVo goods = goodsService.getGoodsVoById(goodsId);
        int stock=goods.getStockCount();
        if (stock<=0){
            return;
        }
        //判断是否秒杀到了
        SeckillOrder order = orderService.getOrderByUserIdGoodsId(user.getId(), goodsId);
        if (order!=null){
            return;
        }
        //减库存 下订单 写入秒杀订单
        seckillService.seckill(user,goods);
    }

上面的方法是RabbitMQ监听队列SECKILL_QUEUE,按照顺序,从队列中读取数据同步数据库操作。

/**
     * 客户端轮询查询是否下单成功
     * orderId:成功
     * -1:秒杀失败
     * 0: 排队中
     */
    @RequestMapping(value = "/result",method = RequestMethod.GET)
    @ResponseBody
    public Result<Long> seckillResult(@RequestParam("goodsId")long goodsId,User user){
        if (user==null){
            return Result.error(CodeMsg.USER_NO_LOGIN);
        }
        long result = seckillService.getSeckillResult(user.getId(), goodsId);
        return Result.success(result);
    }

前端轮训查询是否下单成功,最终查询的是数据库中的数据,而不是Redis中的数据。

/**
     * 获取秒杀地址
     * 自定义接口限流:5秒内最多访问5次,并需要为登录状态
     * @param user
     * @param goodsId
     * @return
     */
    @AccessLimit(seconds = 5,maxCount = 5,needLogin = true)
    @RequestMapping(value = "/path",method = RequestMethod.GET)
    @ResponseBody
    public Result<String> getSeckillPath(User user,@RequestParam("goodsId")long goodsId){
        if (user==null){
            return Result.error(CodeMsg.USER_NO_LOGIN);
        }
        String path = seckillService.createPath(user, goodsId);
        return Result.success(path);
    }

上面是获取秒杀地址的接口,因为主要的并发压力在这个接口,所以需要对这个接口进行限流。

核心点在@AccessLimit这个自定义注解中,来看一下自定义注解的定义

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {
    int seconds();
    int maxCount();
    boolean needLogin() default true;
}

注解不解释了,里面定义了三个方法,seconds(),maxCount(),needLogin(),除了第三个方法从字面意思上能知道是干啥用的,其余头俩都不清楚,这就引出了Spring一个很重要的特性,面向切面编程。

找到AccessInterceptor.java这个类,实现HandlerInterceptor接口,实现preHandle方法

@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof  HandlerInterceptor){
            //获取用户,并保存
            User user = getUser(request, response);
            UserContext.setUser(user);

            //获取限流注解
            HandlerMethod hm = (HandlerMethod) handler;
            AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
            if (accessLimit==null){
                return true;
            }
            //自定义接口限流:时间、访问数、是否需要登录
            //在conttoller方法上加上@AccessLimit(second=5,maxCount=5,needLogin=true)
            int seconds = accessLimit.seconds();
            int maxCount = accessLimit.maxCount();
            boolean needLogin = accessLimit.needLogin();
            String key = request.getRequestURI();
            if (needLogin){
                if (user==null){
                    render(response, CodeMsg.SESSION_ERROR);
                    return false;
                }
                key+="_"+user.getId();
            }else{
                //do nothing
            }
            //根据限流键值获取缓存
            AccessKey ak = AccessKey.withExpire();
            Integer count = redisService.get(ak, key, Integer.class);
            if (count==null){
                redisService.set(ak,key,1,seconds);
            } else if (count<maxCount) {
                redisService.incr(ak,key);
            }else{
                render(response,CodeMsg.ACCESS_LIMIT_REACHED);
                return false;
            }
        }
        return true;
    }

Spring中AOP的概念将在上面的代码中体现的淋漓尽致,首先是if判断是否继承自HandlerInterceptor,然后从request中获取token,进而得到当前的用户,得到用户后与ThreadLocal进行绑定,Threadlocal的底层是一个Map的结构。

随后我们基于当前的handler处理器得到方法的注解,通过这个对象的获取,我们可以拿到注解中各个参数的值。

如果说注解标记的方法需要登录后才能使用,恰巧获取的当前用户为空,需要返回给界面一些提示信息 ,比如像下面代码这样写

/**
     * 把提示返回给客户端
     * @param response
     * @param cm
     * @throws Exception
     */
    private void render(HttpServletResponse response, CodeMsg cm) throws Exception{
        response.setContentType("application/json;charset=UTF-8");
        OutputStream out = response.getOutputStream();
        String str = JSON.toJSONString(Result.error(cm));
        out.write(str.getBytes("UTF-8"));
        out.flush();
        out.close();
    }

之后我们需要根据限流键值对从redis中获取此时这个方法目前已被访问的次数。

值得一提的是redisService.incr以及redisService.decr都是原子方法。

至此切面写完了,我们需要把切面注入到Spring中

来到WebConfig.java,实现WebMvcConfigurer接口,需要重写addArgumentResolvers还有addInterceptors。

addInterceptors这个方法是将自定义的accessInterceptor注册进来就可以

@Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(accessInterceptor);
    }

addArgumentReslvers这个是对Controller层传入的参数进行处理,将处理好的参数传给Controller里面的方法。详情请查看《WebMvcConfigurer中addArgumentResolvers方法的使用》

我们在这里使用的是自己定义的UserArgumentResolver.java这个类。

@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        Class<?> clazz = parameter.getParameterType();
        return clazz== User.class;
    }
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        return UserContext.getUser();
    }
}

我们来简单介绍一下这个实现了HandlerMethodArgumentResolver接口的自定义方法,只有参数是User.class时才会生效,生效后会返回一个User对象。要想深入理解可参考:

https://blog.csdn.net/ocean35/article/details/105892788

结束----

自定义全局异常

自定义异常类在项目中会经常遇到,主要帮助用户抛出自定义的异常,方便用户理解。

public class GlobalException extends  RuntimeException{


    private static final long serialVersionUID=1L;

    private CodeMsg cm;

    public GlobalException(CodeMsg cm){
        super(cm.toString());
        this.cm=cm;
    }
    public CodeMsg getCm(){
        return cm;
    }
}

因为继承自RuntimeException所以必须有个super方法。

Redis配置类

这里使用了Jedis的直接读且application.properties文件的方法,比较新颖,如果以后在项目中碰到业务场景需加入redis并且要求配置简便的情况下,可以考虑这种

首先是RedisConfig这个配置 类,主要是与application.properties中的配置字段对应上。

@Data
@Component
@ConfigurationProperties(prefix = "redis")
public class RedisConfig {
    private String host;
    private int port;
    private int timeout;
    private String password;
    private int poolMaxTotal;
    private int poolMaxIdle;
    private int poolMaxWait;
}

@ConfigurationProperties(prefix="redis")这与application.properties相对应。

接下来是jedisPool创建操作

@Service
public class RedisPoolFactory {
    @Autowired
    private RedisConfig redisConfig;

    @Bean
    public JedisPool JedisPoolFactory(){
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxIdle(redisConfig.getPoolMaxIdle());
        poolConfig.setMaxTotal(redisConfig.getPoolMaxTotal());
        poolConfig.setMaxWaitMillis(redisConfig.getPoolMaxWait()*1000);
        JedisPool jp = new JedisPool(poolConfig, redisConfig.getHost(), redisConfig.getPort(),
                redisConfig.getTimeout() * 1000, redisConfig.getPassword(), 0);
        return jp;
    }
}

核心的一点就是创建JedisPool操作。

redis分布式锁解析

知识点

setIfAbsent(key,value,时长,时长单位):设置之前先判断key值是否存在

setIfAbsent  是redis(setnx)在java中的用法

思路

1.根据秒杀的业务场景,我们需要对秒杀商品的库存数生成一个锁,更改库存数的时候先判断库存锁是否有效和存在

2.如果库存锁存在返回一个错误提示

3.如果库存锁不存在,对库存数量进行操作

4.执行完整个逻辑后删除库存锁

锁的设计

stringRedisTemplate.opsForValue().setIfAbsent(realKey,str,exTime, TimeUnit.SECONDS);

realKey作为key

str作为value

exTime代表过期时间

TimeUnit.SECONDs代表时间单位

WebMvcConfigurer中addArgumentResolvers方法的使用

        在Springboot中的WebMvcConfigurer接口在Web开发中经常被使用,例如配置拦截器、配置ViewController、配置Cors跨域等

本文主要讲解另一个方法:addArgumentResolvers()在实例中的应用。

一、方法作用 该方法可以用在对于Controller中方法参数传入之前对该参数进行处理。然后将处理好的参数在传给Controller中的方法。 官方API文档解释:添加解析器以支持自定义控制器方法参数类型。 这不会覆盖对解析处理程序方法参数的内置支持。要自定义对参数解析的内置支持,请RequestMappingHandlerAdapter直接配置。

二、场景描述 在权限场景中,通常会有要求用户登录之后才能访问的场景。对于这些问题可以多种解决方案,如:使用Cookie+Session的会话控制、使用拦截器、使用SpringSecurity或shiro等权限管理框架等。 这里使用Cookie+Session处理。处理的逻辑为: 用户第一次登录之后会得到一个cookie,在以后每次的访问过程中都会携带Cookie进行访问。在后台的Controller中对于需要登录权限的访问接口都要先获取Cookie中的Token,再使用Token从session中获取用户登录信息来判断用户登录情况决定是否放行。

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

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

相关文章

【软考-中级】系统集成项目管理工程师 【16 变更管理】

持续更新。。。。。。。。。。。。。。。 【第十六章】变更管理 (选择2分 考点 1:变更的常见原因考点 2:变更管理的原则是项目基准化、变更管理过程规范化考点 3考点 4考点 5:变更的工作程序考点 6考点 7考点 8考点 9考点 10考点 11考点 12:变更分类系列文章经典语录 考点 1:变…

使用Python+selenium实现第一个自动化测试脚本

这篇文章主要介绍了使用Pythonselenium实现第一个自动化测试脚本&#xff0c;文中通过示例代码介绍的非常详细&#xff0c;对大家的学习或者工作具有一定的参考学习价值&#xff0c;需要的朋友们下面随着小编来一起学习学习吧 最近在学web自动化&#xff0c;记录一下学习过程。…

在Linux中,怎么查看自己电脑的系统架构是什么?

2023年10月18日&#xff0c;周三晚上 这些命令会返回一个字符串&#xff0c;表示系统的架构。 常见的架构包括 x86&#xff08;32位&#xff09;、x86_64&#xff08;64位&#xff09;、ARM 等。 方法1&#xff1a;使用uname命令 uname -m方法2&#xff1a;使用arch命令 ar…

现代 ERP 系统,如何使中小企业智能制造商受益?

中小企业智能制造商大多依靠手工操作或电子表格模式&#xff0c;或少数几个软件组成的集合体&#xff0c;或是依靠传统的ERP系统来管理企业运营。经营利润率低、订单到现金的周期缓慢、客户付款延迟、管理成本增加&#xff0c;使他们的生存变得更加困难。许多企业一直在以最少的…

uni-app通过 vuedraggable 创建上下拖动排序组件

我们右键项目 选择 使用命令行窗口打开所在目录 然后 在终端中输入 npm install vuedraggable --save导入 vuedraggable 然后组件编写代码如下 <template><view class"container"><draggable v-model"list" :options"dragOptions&…

自动化测试总计

最近要在新入职的公司准备一份自动化测试的培训&#xff0c;这是我在得知要做自动化测试培训以后&#xff0c;随手画了个图&#xff0c;压压惊&#xff1a; 这是我能想到的关于自动化测试的一些要点&#xff0c;然后根据一篇我三年前写的关于自动化测试的随笔更新了一下&#x…

网络安全内网渗透之信息收集--systeminfo查看电脑有无加域

systeminfo输出的内容很多&#xff0c;包括主机名、OS名称、OS版本、域信息、打的补丁程序等。 其中&#xff0c;查看电脑有无加域可以快速搜索&#xff1a; systeminfo|findstr "域:" 输出结果为WORKGROUP&#xff0c;可见该机器没有加域&#xff1a; systeminfo…

【NVIDIA】获取GPU利用率-cpp.md

在深度学习推理中&#xff0c;为了更加高效的利用 GPU&#xff0c;在多个推理任务实例中&#xff0c;创建新的实例以及分配到不同的 GPU 设备上&#xff0c;需要关注到当前 GPU 还有多少剩余&#xff0c;以便更好的分配 代码目录 . ├── CMakeLists.txt ├── src │ └─…

list用法深度解析,一篇文章弄懂list容器各种操作

&#x1f4cb; 前言 &#x1f5b1; 博客主页&#xff1a;在下马农的碎碎念✍ 本文由在下马农原创&#xff0c;首发于CSDN&#x1f4c6; 首发时间&#xff1a;2023/08/10&#x1f4c5; 最近更新时间&#xff1a;2023/08/10&#x1f935; 此马非凡马&#xff0c;房星本是星。向前…

小程序canvas层级过高真机遮挡组件的解决办法

文章目录 问题发现真机调试问题分析问题解决改造代码效果展示 问题发现 在小程序开发中需要上传图片进行裁剪&#xff0c;在实际真机调试中发现canvas层遮挡住了生成图片的按钮。 问题代码 <import src"../we-cropper/we-cropper.wxml"></import> <…

如何使用pytorch定义一个多层感知神经网络模型——拓展到所有模型知识

# 导入必要的库 import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, random_split import torchvision.transforms as transforms import torchvision.datasets as datasets# 定义MLP模型 class MLP(nn.Module):def __…

基于Qt QSlider滑动条小项目

QSlider 是滑动条控件,滑动条可以在一个范围内拖动,并将其位置转换为整数 1. 属性和方法 QSlider 继承自 QAbstractSlider,它的绝大多数属性都是从 QAbstractSlider 继承而来的。 2.QSlider信号 - `valueChanged(int value)`: 当滑块的值改变时发出信号,传递当前滑块的值…

mysql检验分区性能的操作

mysql检验分区性能的操作 创建两个结构相同但是一个有分区另外一个没有分区的表 如上图我们给part_tab5创建的分区为1024个&#xff0c;因为mysql中允许最多有1024个分区&#xff1b;之前我测试的是创建8个分区&#xff0c;然后插入500万条数据&#xff0c;然后按照id查询&…

关于页面优化

一、 js优化 js文件内部 1、减少重复代码的使用&#xff0c;精简代码 2、减少请求次数&#xff0c;如果不是需要实时的数据&#xff0c;可以将请求结果缓存在js变量中&#xff0c;后续直接使用变量的值 3、减少不必要的dom操作&#xff0c;例如&#xff1a;用innerHTMl代替do…

小魔推短视频裂变工具,如何帮助实体行业降本增效?

在如今的互联网时代&#xff0c;大多数的实体老板都在寻找不同的宣传方法来吸引客户&#xff0c;现在短视频平台已经成为重中之重的获客渠道之一&#xff0c;而如何在这个日活用户超7亿的平台获取客户&#xff0c;让更多人知道自己的门店、自己的品牌&#xff0c;泽成为了不少老…

uniapp vue3 使用pinia存储数据

import { defineStore } from pinia;export const userInfo defineStore(userInfo, {state: () > {return {userToken: uni.getStorageSync(token) || ,};},actions: {// 添加tokenupdateToken(token: string) {uni.setStorageSync(token, token);this.userToken token}} …

Apache Doris (四十三): Doris数据更新与删除 - Update数据更新

🏡 个人主页:IT贫道_大数据OLAP体系技术栈,Apache Doris,Clickhouse 技术-CSDN博客 🚩 私聊博主:加入大数据技术讨论群聊,获取更多大数据资料。 🔔 博主个人B栈地址:豹哥教你大数据的个人空间-豹哥教你大数据个人主页-哔哩哔哩视频 目录 1. Update数据更新原理

全面解决找不到vcruntime140_1.dll无法执行此代码问题的5方法

vcruntime140_1.dll是一个动态链接库文件&#xff0c;它是Microsoft Visual C 2015 Redistributable的一部分。当计算机中缺少这个文件时&#xff0c;可能会导致一些应用程序无法正常运行&#xff0c;从而影响我们的工作和生活。 一、问题场景 1. 在使用Windows操作系统的过程…

QTday02(常用类、UI界面下的开发、信号与槽)

今日任务 1. 使用手动连接&#xff0c;将登录框中的取消按钮使用qt4版本的连接到自定义的槽函数中&#xff0c;在自定义的槽函数中调用关闭函数 将登录按钮使用qt5版本的连接到自定义的槽函数中&#xff0c;在槽函数中判断ui界面上输入的账号是否为"admin"&#x…