玩转 Spring Boot 应用篇(搭建菜菜的店铺)

SpringBoot(看这一篇就够了) Spring是一个非常优秀的轻量级框架,以IOC(控制反转)和AOP(面向切面)为思想内核,极大简化了JAVA企业级项目的开发。虽然Spring的组件代码是轻量级的,但它的配置却是重量级的。使用Spring进行项目开发需要在配置文件中写很多代码,所有这些配置都代表了开发时的损耗。除此之外,Spring项目的依赖管理也是一件耗时耗力的事情。在环境搭建时,需要分析要导入哪些库的坐标,而且还需要分析导入与之有依赖关系的其他库的坐标,一旦选错了依赖的版本,随之而来的不兼容问题就会严重阻碍项目的开发进度。比如Spri 阅读详情

0. 

5f972c54a459c33b550d3722a4417168.png

0.0. 历史文章整理

玩转 Spring Boot 入门篇

玩转 Spring Boot 集成篇(MySQL、Druid、HikariCP)

玩转 Spring Boot 集成篇(MyBatis、JPA、事务支持)

玩转 Spring Boot 集成篇(Redis)

玩转 Spring Boot 集成篇(Actuator、Spring Boot Admin)

玩转 Spring Boot 集成篇(RabbitMQ)

玩转 Spring Boot 集成篇(@Scheduled、静态、动态定时任务)

玩转 Spring Boot 集成篇(任务动态管理代码篇)

玩转 Spring Boot 集成篇(定时任务框架Quartz)

玩转 Spring Boot 原理篇(源码环境搭建)

玩转 Spring Boot 原理篇(核心注解知多少)

玩转 Spring Boot 原理篇(自动装配前凑之自定义Starter)

玩转 Spring Boot 原理篇(自动装配源码剖析)

玩转 Spring Boot 原理篇(启动机制源码剖析)

玩转 Spring Boot 原理篇(内嵌Tomcat实现原理&优雅停机源码剖析)

0.1. 背景

菜菜同学前几天与好友一起去环球影城溜达了一天,亲自拍了一系列富有感情的珍藏版照片。为了能更好的输出高质量的技术文章,菜菜想要把照片挂到网站上卖掉来换点银子,然后把写作装备更新一波。

不过,菜菜同学需要快速搭建一个商品售卖网站(菜菜的店铺),以便能够把照片尽快卖掉换点银子 ... ...

未曾想建设店铺途中,遇到了超卖、高并发以及瞬间过高的请求导致访问高峰等一系列的问题,不过终究都被菜菜给化解啦,咱们后面慢慢去谈。

0.2. 技术选型

  • 技术选型:Spring Boot 2.6.3 + MyBatis 3.5.9 + Thymeleaf 3.0.14 + Bootstrap 5.1.3 +Redis 6.2.6+ RabbitMQ 3.9.13 + MySQL 

  • 依赖环境:JDK 1.8 + Maven 3.6.3

  • 开发工具:IntelliJ IDEA

0.3. 项目演示

  • 店铺首页

38158051a41afad6645cc361d6de966c.png

  • 点击“买它”按钮,成功购买页面效果。

891946738a1f8895d7b8b8b18008068c.png

  • 点击“买它”按钮,购买失败页面效果。

c72610ebc8c8c73a9ed9cbc309f72cd4.png

1. 从 0 开始,动手操练起来。

1.1. 设计数据库表

SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;


-- ----------------------------
--  Table structure for `t_goods`
-- ----------------------------
DROP TABLE IF EXISTS `t_goods`;
CREATE TABLE `t_goods` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品编号',
  `name` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT '商品名称',
  `image` varchar(255) CHARACTER SET utf8 NOT NULL COMMENT '商品图片',
  `stock` int NOT NULL DEFAULT '0' COMMENT '商品数量',
  `price` decimal(10,2) DEFAULT NULL COMMENT '价格',
  `start_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '秒杀开始时间',
  `end_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '秒杀结束时间',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;


-- ----------------------------
--  Records of `t_goods`
-- ----------------------------
BEGIN;
INSERT INTO `t_goods` VALUES ('1', '功夫熊猫', '/gfxm.jpg', '10', '666.66', '2022-04-06 17:41:24', '2022-04-06 17:41:24', '2022-04-06 17:41:24', '2022-04-06 17:41:24'), ('2', '威震天', '/wzt.jpg', '8', '888.88', '2022-04-06 17:41:28', '2022-04-06 17:41:28', '2022-04-06 17:41:28', '2022-04-06 17:41:28'), ('3', '小黄人乐翻天', '/xhr.jpg', '6', '777.77', '2022-04-06 17:41:32', '2022-04-06 17:41:32', '2022-04-06 17:41:32', '2022-04-06 17:41:32');
COMMIT;


-- ----------------------------
--  Table structure for `t_user_goods`
-- ----------------------------
DROP TABLE IF EXISTS `t_user_goods`;
CREATE TABLE `t_user_goods` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '唯一主键',
  `user_id` bigint DEFAULT NULL COMMENT '用户id',
  `goods_id` bigint DEFAULT NULL COMMENT '商品id',
  `quantity` int DEFAULT '0' COMMENT '数量',
  `state` tinyint DEFAULT NULL COMMENT '状态,-1:无效;0:成功;1:已付款',
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;


SET FOREIGN_KEY_CHECKS = 1;

数据库表设计很简单,一张商品信息表,一张用户购买记录表。

1.2. 创建项目骨架

采用 IDEA 创建 Spring Boot Web 项目(引入 web 依赖包)

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>

项目名为 caicaishop。

bbe798930dd60c5c843bdccaddf736e2.png

项目创建后,直接运行 main 函数,看看环境是否 OK,正常启动会输出。

93c569558f78b872bc5ee42684d44437.png

1.3. 集成 MyBatis

  • pom.xml 加入依赖

<!-- 引入 MyBatis 依赖-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybaits-spring-boot-starter</artifactId>
    <version>2.2.1</version>
</dependency>


<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
  • 添加数据库连接& MyBatis 配置

在 application.properties 文件中添加配置信息。

# MySQL 链接信息
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/caicaishop?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver


## MyBatis 的配置
# Mapper资源文件存放的路径
mybatis.mapper-locations=classpath*:mapper/*Mapper.xml
# Dao 接口文件存放的目录
mybatis.type-aliases-package=org.growup.caicaishop.dao
# 开启 debug,输出 SQL
logging.level.org.growup.caicaishop.dao=debug

1.4. 创建实体类

首先创建 entity 目录,用于存放实体类源代码文件。

  • 创建 Goods 实体类

/**
 * 商品信息类
 */
public class Goods implements Serializable {


    private Integer id;


    private String name;


    private String image;


    private Integer stock;


    private BigDecimal price;


    private Timestamp startTime;


    private Timestamp endTime;


    private Timestamp createTime;


    private Timestamp updateTime;


    // 提供 setter / getter 方法
}
  • 创建 UserGoods 实体类

/**
 * 用户商品购买记录
 */
public class UserGoods implements Serializable {


    private Integer id;


    private Integer userId;


    private Integer goodsId;


    private Integer quantity;


    private Integer state;


    private Timestamp createTime;


    private Timestamp updateTime;


    // 提供 setter / getter 方法
    // 提供 toString 方法
}

1.5. 创建 Dao

  • 创建商品Dao(GoodsDao)

提供查询单个商品、查询所有商品以及减库存的方法定义。

package org.growup.caicaishop.dao;


import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.growup.caicaishop.entity.Goods;


import java.util.List;


@Mapper
public interface GoodsDao {


    /**
     * 查询商品
     */
    public Goods getGoodsById(@Param("id")Integer id);


    /**
     * 查询所有商品
     */
    public List<Goods> selectAll();


    /**
     * 减库存
     */
    public int reduceStock(@Param("id")Integer id, @Param("quantity") int quantity);
}
  • 创建用户商品购买记录Dao(UserGoodsDao)

提供保存用户购买记录的方法定义。

package org.growup.caicaishop.dao;


import org.apache.ibatis.annotations.Mapper;
import org.growup.caicaishop.entity.UserGoods;


@Mapper
public interface UserGoodsDao {
    /**
     * 插入用户购买记录
     */
    public int insert(UserGoods userGoods);
}

1.6. 创建 Mapper 文件

在 resources 目录下创建 mapper 文件夹,用于存放 mapper 文件。

  • GoodsMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.growup.caicaishop.dao.GoodsDao">


    <resultMap id="BaseResultMap" type="org.growup.caicaishop.entity.Goods">
        <id column="id" property="id" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <result column="image" property="image" jdbcType="VARCHAR"/>
        <result column="stock" property="stock" jdbcType="INTEGER"/>
        <result column="price" property="price" jdbcType="DECIMAL"/>
        <result column="start_time" property="startTime" jdbcType="TIMESTAMP"/>
        <result column="end_time" property="endTime" jdbcType="TIMESTAMP"/>
        <result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
        <result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
    </resultMap>


    <select id="getGoodsById" resultMap="BaseResultMap">
        select id, name, image, stock, price,start_time,end_time,create_time, update_time from t_goods where id = #{id,jdbcType=INTEGER}
    </select>


    <select id="selectAll" resultMap="BaseResultMap">
        select id, name, image, stock, price,start_time,end_time,create_time, update_time from t_goods
    </select>


    <update id="reduceStock">
        update t_goods set stock = stock - #{quantity} where id = #{id,jdbcType=INTEGER}
    </update>
</mapper>
  • UserGoodsMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.growup.caicaishop.dao.UserGoodsDao">
    <insert id="insert" parameterType="org.growup.caicaishop.entity.UserGoods">
        insert into t_user_goods (user_id, goods_id, quantity, state, create_time)
        values (#{userId,jdbcType=INTEGER}, #{goodsId,jdbcType=INTEGER}, #{quantity,jdbcType=INTEGER},
                #{state,jdbcType=INTEGER}, #{createTime,jdbcType=TIMESTAMP})
    </insert>
</mapper>

1.7. 创建 Service 接口

  • 商品Service(GoodsService)

package org.growup.caicaishop.service;


import org.growup.caicaishop.entity.Goods;


import java.util.List;


public interface GoodsService {


    /**获取商品列表*/
    public List<Goods> findAllGoods();


    /**减库存*/
    public int reduceStock(Integer goodsId,int quantity);
}
  • 用户商品购买记录Service(UserGoodsService)

package org.growup.caicaishop.service;


import org.growup.caicaishop.entity.UserGoods;


public interface UserGoodsService {
    /**
     * 保存用户购买记录
     */
    public int save(UserGoods userGoods);
}
  • 处理购买业务的Service(PurchaseService)

package org.growup.caicaishop.service;


public interface PurchaseService {
    /**
     * 处理购买业务
     */
    public boolean purchase(Integer userId, Integer goodsId, int quantity);
}

1.8. 创建 Service 接口的实现类

  • 商品Service实现类(GoodsServiceImpl)

package org.growup.caicaishop.service.impl;


import org.growup.caicaishop.dao.GoodsDao;
import org.growup.caicaishop.entity.Goods;
import org.growup.caicaishop.service.GoodsService;
import org.springframework.stereotype.Service;


import javax.annotation.Resource;
import java.util.List;


@Service
public class GoodsServiceImpl implements GoodsService {


    @Resource
    private GoodsDao goodsDao;


    @Override
    public List<Goods> findAllGoods() {
        return goodsDao.selectAll();
    }


    @Override
    public int reduceStock(Integer goodsId,int quantity) {
        return goodsDao.reduceStock(goodsId,quantity);
    }
}
  • 用户购买商品记录Service实现类(UserGoodsServiceImpl)

package org.growup.caicaishop.service.impl;


import org.growup.caicaishop.dao.UserGoodsDao;
import org.growup.caicaishop.entity.UserGoods;
import org.growup.caicaishop.service.UserGoodsService;
import org.springframework.stereotype.Service;


import javax.annotation.Resource;


@Service
public class UserGoodsServiceImpl implements UserGoodsService {


    @Resource
    private UserGoodsDao userGoodsDao;


    @Override
    public int save(UserGoods userGoods) {
        return userGoodsDao.insert(userGoods);
    }
}
  • 购买逻辑处理Service实现类(PurchaseServiceImpl)

package org.growup.caicaishop.service.impl;


import org.growup.caicaishop.dao.GoodsDao;
import org.growup.caicaishop.dao.UserGoodsDao;
import org.growup.caicaishop.entity.Goods;
import org.growup.caicaishop.entity.UserGoods;
import org.growup.caicaishop.service.PurchaseService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


import javax.annotation.Resource;
import java.sql.Timestamp;
import java.util.logging.Logger;


@Service
public class PurchaseServiceImpl implements PurchaseService {


    private final Logger logger = Logger.getLogger("PurchaseServiceImpl");


    @Resource
    private GoodsDao goodsDao;


    @Resource
    private UserGoodsDao userGoodsDao;


    @Override
    @Transactional
    public boolean purchase(Integer userId, Integer goodsId, int quantity) {
        Goods goodsInfo = goodsDao.getGoodsById(goodsId);
        if (goodsInfo.getStock() < quantity) {
            // 库存不足
            logger.info("库存不足: " + goodsInfo.getStock());
            return false;
        }


        int res = goodsDao.reduceStock(goodsId, quantity);
        logger.info("扣减库存结果:" + res);
        //插入购买记录
        UserGoods userGoods = new UserGoods();
        userGoods.setUserId(userId);
        userGoods.setGoodsId(goodsId);
        userGoods.setQuantity(quantity);
        userGoods.setState(1);
        userGoods.setCreateTime(new Timestamp(System.currentTimeMillis()));
        int saveRes = userGoodsDao.insert(userGoods);
        logger.info("插入购买记录:" + saveRes);
        return saveRes == 1;
    }
}

至此,实体类、Dao、Service 等数据库相关封装操作基本完事儿,跑单元测试验证一下。

1.9. 单元测试验证

package org.growup.caicaishop;


import org.growup.caicaishop.entity.Goods;
import org.growup.caicaishop.entity.UserGoods;
import org.growup.caicaishop.service.GoodsService;
import org.growup.caicaishop.service.PurchaseService;
import org.growup.caicaishop.service.UserGoodsService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;


import java.sql.Timestamp;
import java.util.List;
import java.util.logging.Logger;


@SpringBootTest
class CaicaishopApplicationTests {


    private final Logger logger = Logger.getLogger("CaicaishopApplicationTests");


    @Autowired
    private GoodsService goodsService;


    @Autowired
    private UserGoodsService userGoodsService;


    @Autowired
    private PurchaseService purchaseService;


    @Test
    public void testFindAllGoods() {
        List<Goods> goodsList = goodsService.findAllGoods();
        logger.info("商品信息:" + goodsList);
    }


    @Test
    public void testSaveUserGoods() {
        UserGoods userGoods = new UserGoods();
        userGoods.setUserId(10086);
        userGoods.setGoodsId(1);
        userGoods.setQuantity(1);
        userGoods.setState(1);
        userGoods.setCreateTime(new Timestamp(System.currentTimeMillis()));
        logger.info("保存用户购买商品记录结果:" + userGoodsService.save(userGoods));
    }


    @Test
    public void testPurchase() {
        boolean purchaseRes = purchaseService.purchase(1,1,1);
        logger.info("商品购买结果:" + purchaseRes);
    }
}
  • 验证 GoodService 中获取商品列表功能。

fe0cb4afbb14f9dfea1422da200f1ecd.png

  • 验证 UserGoodsService 中保存用户购买商品记录功能。

8c811624cdc1fc58f047144553e6a9c4.png

  • 验证 PurchaseService 中商品购买功能。

6c5897f3d4a0d01185b914bc12ca0017.png

至此,数据库层面的 CRUD 封装完成,验证通过。

2. 控制层实现

  • 首页控制器(IndexController)

package org.growup.caicaishop.controller;


import org.growup.caicaishop.service.GoodsService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;


import javax.annotation.Resource;


@Controller
public class IndexController {


    @Resource
    private GoodsService goodsService;


    @RequestMapping({"","/","/index"})
    public String index(Model model) {
        model.addAttribute("goodsList", goodsService.findAllGoods());
        return "index";
    }
}

主要是查询商品列表,然后返回给 index 视图。

  • 商品购买控制器(PurchaseController)

package org.growup.caicaishop.controller;


import org.growup.caicaishop.service.PurchaseService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;


import javax.annotation.Resource;


@Controller
public class PurchaseController {


    @Resource
    private PurchaseService purchaseService;


    /**
     * 购买商品,直接返回视图
     */
    @RequestMapping("/purchase")
    public String purchase(Model model, @RequestParam("goodsId") Integer goodsId,
                           @RequestParam("userId") Integer userId) {
        boolean isOk = purchaseService.purchase(userId, goodsId, 1);
        model.addAttribute("msg", isOk ? "恭喜您,购买成功^_^" : "很遗憾,别灰心,继续买...");
        return "notice";
    }


    /**
     * 购买商品 API,供前端模拟并发使用
     */
    @CrossOrigin
    @ResponseBody
    @RequestMapping("/api/purchase")
    public String purchaseAPI(Integer goodsId, Integer userId) {
        boolean isOk = purchaseService.purchase(userId, goodsId, 1);
        return isOk ? "恭喜您,购买成功^_^" : "很遗憾,别灰心,继续买...";
    }
}

提供了两种方式:一种是直接返回视图,一种是供模拟并发调用的 API 。

3. 集成 Thymeleaf & 展示层实现

3.1 集成 Thymeleaf

Thymeleaf 是一个 Java XML / XHTML / HTML5 模板引擎 ,可以在 Web(基于servlet )和非 Web 环境中工作。它更适合在基于 MVC 的 Web 应用程序的视图层提供 XHTML / HTML5,但它甚至可以在脱机环境中处理任何 XML 文件。它提供完整的 Spring Framework。

在 Web 应用程序中,Thymeleaf 旨在成为 JavaServer Pages(JSP)的完全替代品,并实现自然模板的概念:模板文件可以直接在浏览器中打开,并且仍然可以正确显示为网页。

  • 引入 thymeleaf 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  • thymeleaf 相关配置

在 application.properties 文件中加入如下配置。

### thymeleaf 配置
# 模板的模式,支持如 HTML、XML、TEXT、JAVASCRIPT 等
spring.thymeleaf.mode=HTML
# 编码,可不用配置
spring.thymeleaf.encoding=UTF-8
# 内容类别,可不用配置
spring.thymeleaf.servlet.content-type=text/html
# 开发环境下配置为 false,避免修改模板还需要重启服务器
spring.thymeleaf.cache=false
# 配置模板路径,默认就是 templates,可不用配置
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

3.2 展示层实现

  • 店铺首页(index.html)

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>goods list</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="/css/pricing.css" rel="stylesheet" type="text/css">
</head>


<body>
<div class="container py-3">
    <header>
        <div class="d-flex flex-column flex-md-row align-items-center pb-3 mb-4 border-bottom">
            <a href="/" class="d-flex align-items-center text-dark text-decoration-none">
                <svg xmlns="http://www.w3.org/2000/svg" width="40" height="32" class="me-2" viewBox="0 0 118 94"
                     role="img"><title>CaiCaiShop</title>
                    <path fill-rule="evenodd" clip-rule="evenodd"
                          d="M24.509 0c-6.733 0-11.715 5.893-11.492 12.284.214 6.14-.064 14.092-2.066 20.577C8.943 39.365 5.547 43.485 0 44.014v5.972c5.547.529 8.943 4.649 10.951 11.153 2.002 6.485 2.28 14.437 2.066 20.577C12.794 88.106 17.776 94 24.51 94H93.5c6.733 0 11.714-5.893 11.491-12.284-.214-6.14.064-14.092 2.066-20.577 2.009-6.504 5.396-10.624 10.943-11.153v-5.972c-5.547-.529-8.934-4.649-10.943-11.153-2.002-6.484-2.28-14.437-2.066-20.577C105.214 5.894 100.233 0 93.5 0H24.508zM80 57.863C80 66.663 73.436 72 62.543 72H44a2 2 0 01-2-2V24a2 2 0 012-2h18.437c9.083 0 15.044 4.92 15.044 12.474 0 5.302-4.01 10.049-9.119 10.88v.277C75.317 46.394 80 51.21 80 57.863zM60.521 28.34H49.948v14.934h8.905c6.884 0 10.68-2.772 10.68-7.727 0-4.643-3.264-7.207-9.012-7.207zM49.948 49.2v16.458H60.91c7.167 0 10.964-2.876 10.964-8.281 0-5.406-3.903-8.178-11.425-8.178H49.948z"
                          fill="currentColor"/>
                </svg>
                <span class="fs-4 text-success">菜菜的店铺</span>
            </a>
        </div>


        <div class="pricing-header p-3 pb-md-4 mx-auto text-center">
            <h1 class="display-4 fw-normal text-success">生活就是买买买</h1>
            <p/>
            <ul class="list-group list-group-horizontal">
                <li class="list-group-item list-group-item-danger">有钱不花,掉了白搭</li>
                <li class="list-group-item list-group-item-warning">钱是王八蛋,没了咱再赚</li>
                <li class="list-group-item list-group-item-info">每天不是买买买,就是去买买买的路上</li>
            </ul>
        </div>
    </header>


    <main>
        <div class="row row-cols-1 row-cols-md-3 mb-3 text-center">
            <div class="col" th:each="goods:${goodsList}">
                <div class="card mb-4 rounded-3 shadow-sm border-success">
                    <div class="card-header py-3 text-white bg-success border-success">
                        <h4 class="my-0 fw-normal" th:text="${goods.name}"></h4>
                    </div>
                    <div class="card-body">
                        <h1 class="card-title pricing-card-title">
                            $ <small class="text-success fw-light" th:text="${goods.price}"/>
                        </h1>
                        <ul class="list-unstyled mt-3 mb-4">
                            <li>库存剩余:<span class="badge bg-danger badge-pill" th:text="${goods.stock}"/></li>
                            <li><img width="110px" height="120px" th:src="@{'/images'+${goods.image}}"/></li>
                            <li>开始时间:<small class="text-success fw-light" th:text="${goods.startTime}"/></li>
                            <li>结束时间:<small class="text-success fw-light" th:text="${goods.endTime}"/></li>
                        </ul>
                        <a type="button" class="w-100 btn btn-lg btn-outline-success"
                           th:href="@{'/purchase?userId=1&goodsId='+${goods.id}}">买它</a>
                    </div>
                </div>
            </div>
        </div>
    </main>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js"/>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"/>
</html>
  • 商品购买通知页面(notice.html)

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>purchase result</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="/css/pricing.css" rel="stylesheet" type="text/css">
</head>


<body>
<div class="container py-3">
    <header>
        <div class="d-flex flex-column flex-md-row align-items-center pb-3 mb-4 border-bottom">
            <a href="/" class="d-flex align-items-center text-dark text-decoration-none">
                <svg xmlns="http://www.w3.org/2000/svg" width="40" height="32" class="me-2" viewBox="0 0 118 94"
                     role="img"><title>CaiCaiShop</title>
                    <path fill-rule="evenodd" clip-rule="evenodd"
                          d="M24.509 0c-6.733 0-11.715 5.893-11.492 12.284.214 6.14-.064 14.092-2.066 20.577C8.943 39.365 5.547 43.485 0 44.014v5.972c5.547.529 8.943 4.649 10.951 11.153 2.002 6.485 2.28 14.437 2.066 20.577C12.794 88.106 17.776 94 24.51 94H93.5c6.733 0 11.714-5.893 11.491-12.284-.214-6.14.064-14.092 2.066-20.577 2.009-6.504 5.396-10.624 10.943-11.153v-5.972c-5.547-.529-8.934-4.649-10.943-11.153-2.002-6.484-2.28-14.437-2.066-20.577C105.214 5.894 100.233 0 93.5 0H24.508zM80 57.863C80 66.663 73.436 72 62.543 72H44a2 2 0 01-2-2V24a2 2 0 012-2h18.437c9.083 0 15.044 4.92 15.044 12.474 0 5.302-4.01 10.049-9.119 10.88v.277C75.317 46.394 80 51.21 80 57.863zM60.521 28.34H49.948v14.934h8.905c6.884 0 10.68-2.772 10.68-7.727 0-4.643-3.264-7.207-9.012-7.207zM49.948 49.2v16.458H60.91c7.167 0 10.964-2.876 10.964-8.281 0-5.406-3.903-8.178-11.425-8.178H49.948z"
                          fill="currentColor"/>
                </svg>
                <span class="fs-4">菜菜的店铺</span>
            </a>
        </div>


        <div class="pricing-header p-3 pb-md-4 mx-auto text-center">
            <h1 class="display-4 fw-normal text-success">生活就是买买买</h1>
            <p/>
            <ul class="list-group list-group-horizontal">
                <li class="list-group-item list-group-item-danger">有钱不花,掉了白搭</li>
                <li class="list-group-item list-group-item-warning">钱是王八蛋,没了咱再赚</li>
                <li class="list-group-item list-group-item-info">每天不是买买买,就是去买买买的路上</li>
            </ul>
        </div>
    </header>
    <main>
        <div class="text-center">
            <h1 class="display-4 fw-normal text-info" th:text="${msg}"/>
        </div>
    </main>
</div>
</body>
</html>
  • css 内容(pricing.css)

body {
  background-image: linear-gradient(180deg, #eee, #fff 100px, #fff);
}


.container {
  max-width: 960px;
}


.pricing-header {
  max-width: 700px;
}
  • 准备图片资源(gfxm.jpg[功夫熊猫]、wzt.jpg[威震天]、xhr.jpg[小黄人闹翻天])

至此,caicaishop 项目的整体结构变的感觉有点像样了。

a811ae10cad8062c779ff32b368474a2.png

4. 启动项目,看看效果

4.1. 体验商品购买

运行 CaiCaiShopApplication 项目入口,直接访问店铺,购买体验顺畅。

8f408a2c5fe663546212e8ff7a636039.png

至此,基于 Spring Boot 搭建的照片(骗)售卖店铺就完成了,菜菜正计划对外推广。

不过,菜菜同学不是一个省油的灯,而且会点前端,准确说也是个前端的二把刀,于是想整个脚本看看网站的并发处理能力。

于是菜菜花了一根烟的功夫用 HTML 编写了一个模拟并发的页面。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>高并发</title>
    <script src="https://cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
</head>
<body>
<div class="text-center">
    <h1 class="display-4 fw-normal text-info"/>
</div>
<script type="text/javascript">
    const userId = 10086;
    const goodsId = 3;
    const times = 1500;
    for (i = 1; i <= times; i++) {
        $.ajax({
            type: "GET",
            url: "http://localhost:8080/api/purchase",
            data: "userId=" + userId + "&goodsId=" + goodsId,
            async: true,
            success: function (msg) {
                console.log(msg);
            }
        });


        if (i === times) {
            $(".text-center").text(times + "次请求发送完毕");
        }
    }
</script>
</body>
</html>

文件保存成 .html,然后用浏览器直接打开,发现坏事儿了。

4.2. 坏事儿了

当用浏览器直接打开菜菜的 html 并发脚本,发现服务端日志偶尔会出现了超卖,控制台输出如下。

9a5ddcadfa9eefc00d507b6dfc4929af.png

数据库发现库存变成负数 。

36ca684a8192f34012206ffefd719cec.png

看到这种效果,菜菜泪奔,本来想拿照片(骗)换点银子,这么下去不得亏大发呀,那该怎么办?各自先自行查查是咋回事?本次不做解答,下次一起揭秘。

5. 例行回顾

本文开始正式迈入 Spring Boot 应用篇,主要是通过项目搭建来熟练前期讲过的 Spring Boot 相关集成技术,随着后续的逐步深入,会一起探究一下悲观锁、乐观锁、秒杀相关的缓存、削峰等相关知识

,看看如何借助这些知识点一步一步来解决业务问题。

生活就是不断突破自我的过程。我们努力地向上,不仅是让世界看到我们,更是为了让自己看到世界。当我们一步一个脚印往前走时会发现,每一点进步,都在让我们的人生变得辽阔。

一起聊技术、谈业务、喷架构,少走弯路,不踩大坑,会持续输出更多精彩分享,欢迎关注,敬请期待!

06b3b3c4474eb83d826e6f914b9e33e6.jpeg

参考资料:

https://spring.io/

https://start.spring.io/

https://spring.io/projects/spring-boot

https://github.com/spring-projects/spring-boot

https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/

https://stackoverflow.com/questions/tagged/spring-boot

《Spring Boot实战》《深入浅出Spring Boot 2.x》

《一步一步学Spring Boot:微服务项目实战(第二版)》

《Spring Boot揭秘:快速构建微服务体系》

新手必看:5分钟搞懂FFT波束形成的底层原理(从阵列信号到频域处理) 本文深入浅出地解析了FFT波束形成的底层原理,从阵列信号采集到频域处理的全过程。通过音乐节场景类比和Python代码示例,帮助新手快速理解多目标波束形成的核心技术,包括频域转换、相位补偿和性能优化策略,适用于雷达、声呐和5G通信等领域。 阅读详情

相关推荐

西门子PLC实战:基于1200/1500的工业物联网MQTT通讯全解析

本文详细解析了如何在西门子S7-1200/1500系列PLC上实现工业物联网MQTT通讯。通过从硬件准备、网络配置到TIA Portal中MQTT_Client功能块的实战编程,手把手指导工程师打通车间数据桥梁,解决信息孤岛问题,实现PLC数据高效上云或设备间互联。

whisky的博客 1090

SpringBoot全栈实战:Thymeleaf整合与邮件短信服务深度解析

本文深度剖析SpringBoot整合Thymeleaf模板引擎的核心技术,详解邮件服务(SMTP/QQ邮箱)与阿里云短信服务的实战集成方案。通过用户CRUD功能实现演示前后端协作,提供Thymeleaf语法精讲、静态资源优化策略及企业级项目打包部署指南。内容涵盖模板缓存配置、复杂邮件发送、短信API对接等高频开发场景,助您快速掌握SpringBoot全栈开发的关键技能与最佳实践。

Zyw907155124的博客 2507

西门子840D新建项目

1、需要一个32位的WIN7或者XP系统,并且已经在系统中安装完成西门子840Dsl开发包和VS2008。(我采用的是XP系统虚拟机) 2、在开始->所有程序找到如下位置 3、在这个位置打开Visual Studio 4、文件->新建->项目 名称和位置都可以随意更改,点击确定。 5、在左侧选择第二个选项进入下面的界面 点击“Add Screen”按钮 可以修改“class name”然后点击“OK”返回上一个界面,再点击“Add Form” 可以修...

qq_36987047的博客 1861

【SpringBoot应用篇】【AOP+注解】SpringBoot集成Mybatis实现多数据源配置+跨数据源事务

开发中经常有这样的需要:。微服务环境下可以实现一个服务读取一个数据库,另一个服务写库。但是在实际应用中有时也需要在一个服务中读写不同的数据库。可以在一个SpringBoot单体项目中配置多个数据源解决读写库分离。

输入技术、输出想法 5489

【AMD显卡在WIndow10/11部署带GPU支持的深度学习环境(Pytorch-DirectML篇)】

这篇是之前Tensorflow-DirectML的姊妹篇,如果对TF感兴趣的可以搜索我的博文。 首先我们丢一个官网地址,但是官网那个会误导新人如果你按那个操作的话。 https://docs.microsoft.com/en-us/windows/ai/directml/gpu-pytorch-windows 这里用我的方法操作: 首先声明一下,因为我的显卡是AMD RX6600,不带XT(性能和RTX3060 12G版本半斤八两),所以目前没有最新的WSL2驱动,如果有了的话欢迎告知我,所以我这里就.

weixin_44029053的博客 2万+

MITRE ATT&CK超详细学习笔记-01(背景,术语,案例)

MITRE是一个向美国政府提供系统工程、研究开发和信息技术支持的非营利性组织,于1958年从麻省理工学院林肯实验室分离出来后参与了许多最高机密的政府项目,开展了大量的网络安全实践。例如,MITRE公司在1999年发起了常见披露漏洞项目(CVE,CommonVulnera-bilitiesandExposures)并维护至今。其后,MITRE公司还维护了常见缺陷列表(CWE,CommonWeaknessEnumeration)这个安全漏洞词典。.........

Zichel77的博客 1万+

SpringBoot的应用

EnableConfigurationProperties:用于启用应用对另外一个注解 @ConfigurationProperties的支持。如果想继续使用 Spring Boot 2.3 的配置逻辑,也可以通过在 application.properties 或者。如果配置文件的名称不叫application.properties或者application.yml,可以通过以下参数来指定。@Configuration:声明一个类作为配置类。@Configuration:声明一个类作为配置类。

xike1024的博客 1775

从零构建:用Simulink搭建固定翼无人机六自由度模型的避坑指南

本文详细介绍了使用Simulink从零构建固定翼无人机六自由度动力学模型的完整流程和避坑指南。重点解析了模型架构设计、气动力计算、参数配置、仿真求解器选择等关键环节的常见问题与解决方案,帮助开发者快速建立准确可靠的无人机控制系统仿真模型。

hhh00的博客 999

SpringBoot的使用

SpringBoot的使用

weixin_45427945的博客 2943

OpenClaw 在 Windows 环境下的完整安装、配置和部署指南

介绍Windows环境下OpenClaw的完整安装,配置和部署的全流程

hxbao的专栏 1486

如何利用Dpabi,AFNI,FSL软件计算种子点的功能连接?

fsl_glm 得到的是线性回归系数(beta coefficient),由于在自变量和因变量标准化(均值为 0,标准差为 1)的情况下,简单线性回归的回归系数和皮尔逊相关系数是相同的,因此这是一种间接的方法。(3)上一步得到的 PCC.nii.gz 文件是 Harvard-Oxford 概率模板中的后扣带回,每个体素中的数值(1-100)表示该体素属于 PCC 的概率,反映 PCC 在不同被试中的变异大小,也表示我们对于某个体素属于PCC 的信心。接下来,我们来聊聊功能连接在数据处理里的具体应用。

从事脑科学核磁共振方法学研究,在Nature communications等权威期刊发表研究论文,熟练掌握磁共振处理方法和统计学方法,欢迎大家和我交流。 7842

快速上手 Spring Boot:基础使用详解

Spring Boot 是一个基于 Spring 的框架,旨在简化 Spring 应用的配置和开发过程,通过自动配置和约定大于配置的原则,使开发者能够快速搭建独立、生产级别的应用程序。Spring程序缺点配置繁琐依赖设置繁琐SpringBoot程序优点自动配置起步依赖(简化依赖配置)版本锁定内置tomcat服务器辅助功能(内置服务器,……)

小林的博客 2万+

2026 年第十六届MathorCup数学应用挑战赛题目C题中老年人群高血脂症的风险预警及干预方案优化 2026妈妈杯 详细思路、代码、模型

2026 年第十六届MathorCup数学应用挑战赛题目 C题中老年人群高血脂症的风险预警及干预方案优化 人口老龄化进程的持续加快,使得40岁及以上中老年人群成为 慢性病防控的核心群体,其中高血脂症的发病率呈逐年攀升态势, 已成为诱发冠心病、脑梗死等心脑血管疾病的重要危险因素,严重 威胁中老年群体的心血管健康与生活质量,也加重了基层医疗的慢 病管理负担。从中医体质学视角来看,体质禀赋与偏颇是疾病发生 发展的内在根基,其中痰湿体质为中老年高血脂症的高发体质类型, 该体质人群存在痰湿内蕴、脾胃运化失常、水湿代谢

YOLO 756

玩转 Spring Boot 应用篇(解决菜菜店铺商品超卖问题)

0.0.0. 历史文章整理玩转 Spring Boot 入门篇玩转 Spring Boot 集成篇(MySQL、Druid、HikariCP)玩转 Spring Boot 集成篇(MyBatis、JPA、事务支持)玩转 Spring Boot 集成篇(Redis)玩转 Spring Boot 集成篇(Actuator、Spring Boot Admin)玩转 Sprin...

一猿小讲 673

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

玩转 Spring Boot 应用篇(项目打包、发布)

0.0.0. 历史文章整理玩转 Spring Boot 入门篇玩转 Spring Boot 集成篇(MySQL、Druid、HikariCP)玩转 Spring Boot 集成篇(MyBatis、JPA、事务支持)玩转 Spring Boot 集成篇(Redis)玩转 Spring Boot 集成篇(Actuator、Spring Boot Admin)玩转 Sprin...

一猿小讲 818

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

玩转 Spring Boot 应用篇(引入RabbitMQ解决店铺下单峰值问题)

0.0.0. 历史文章整理玩转 Spring Boot 入门篇玩转 Spring Boot 集成篇(MySQL、Druid、HikariCP)玩转 Spring Boot 集成篇(MyBatis、JPA、事务支持)玩转 Spring Boot 集成篇(Redis)玩转 Spring Boot 集成篇(Actuator、Spring Boot Admin)玩转 Sprin...

一猿小讲 3383

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

玩转 Spring Boot 应用篇(引入Redis解决店铺高并发读的问题)

0.0.0. 历史文章整理玩转 Spring Boot 入门篇玩转 Spring Boot 集成篇(MySQL、Druid、HikariCP)玩转 Spring Boot 集成篇(MyBatis、JPA、事务支持)玩转 Spring Boot 集成篇(Redis)玩转 Spring Boot 集成篇(Actuator、Spring Boot Admin)玩转 Sprin...

一猿小讲 431

玩转 Spring Boot 应用篇(序列号生成器服务实现)

0.0.0. 历史文章整理玩转 Spring Boot 入门篇玩转 Spring Boot 集成篇(MySQL、Druid、HikariCP)玩转 Spring Boot 集成篇(MyBatis、JPA、事务支持)玩转 Spring Boot 集成篇(Redis)玩转 Spring Boot 集成篇(Actuator、Spring Boot Admin)玩转 Sprin...

一猿小讲 572

Spring Boot应用开发指南

Spring Boot凭借其简洁、高效、易于上手的特点,成为Java应用开发领域的热门框架。本文介绍了Spring Boot的基础知识、核心组件、常用功能以及最佳实践,帮助开发者快速上手并构建高质量的Spring Boot应用。通过遵循良好的代码结构、配置管理、日志管理、单元测试和性能优化等最佳实践,开发者可以进一步提升应用的质量和用户体验。

2301_82150492的博客 1036

SpringBoot介绍以及基本注解和应用

简化Spring应用开发的一个框架;整个Spring技术栈的一个大整合;J2EE开发的一站式解决方案;优点:快速创建独立运行的spring项目以及与主流框架集成使用嵌入式的Servlet容器,应用无需打成war包,内嵌TomcatStarters自动依赖和版本控制大量的自动装配,简化开发,也可以修改默认值无需配置XML无代码生成开箱即用准生产环境的运行时应用监控与云计算的天然集成。

秃头之旅 2531

Spring Boot应用开发详解

Spring Boot是一种基于Spring框架的轻量级开发框架,它简化了Spring应用的开发过程,提供了一套预设的开发规范和约束,并且内置了大量常用的第三方库。Spring Boot的核心思想是“约定大于配置”,它通过封装和自动配置的方式,让开发者可以快速构建一个独立运行的、生产级别的Spring应用。Spring Boot提供了自动化的配置方式,大部分的配置都可以通过约定来实现,不再需要繁琐的配置文件。

shiming8879的博客 1982

《springboot使用篇》——只为使用,一篇就够

Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。Spring Boot 基于Spring开发,Spring Boot 本身并不提供Spring 框架的核心特性以及扩展功能,只是用于快速.敏捷的开发新一代基于Spring 框架的应用程序。

weixin_64972949的博客 2万+

SpringBoot开发Web应用

如果不做任何配置,webapps文件夹的所有文件都不会被编译到target/classes目录下的,更不会被打包到jar包中.这个时候则需要再pom.xml文件的build标签(如果没有就新增)中新增两个resource标签,分别用于打包src/main/webapps资源文件夹与src/main/resource资源文件夹。场景:如若我们使用的servlet等组件是第三方的,jar包中是只读,无法添加注解的,也就无法进行注解扫描,这个时候就得使用配置类的方式。4.5 采用配置类的方式注入组件。

java_qianmo的博客 715

springboot的简单应用

Controller类,由于项目是前后端分离开发的,所以我们需要解决跨域问题,否则游览器会拦截请求,这里简单的将本机的所有请求全部放行。我开始想到的是sql语句里面的连接,但三个连接查询出来的结果全都有问题,后面想到了上课学的association,但是assocaition适应于一对一的关系,这里明显是一对多的关系(一个菜品对应一个店铺,一个店铺对应多个菜品,数据比较极端),所以查资料找到了collection来处理这种一对多的关系。封装返回结果使用了大量的泛型的思想。勾选web和mabais框架。

qq_73841617的博客 1024

SpringBoot概述及项目的创建使用

介绍什么是SpringBoot及其优点,使用IDEA创建SpringBoot项目以及在网页创建项目。

trong_ 的烂笔头 2779

快速上手Spring Boot应用

Spring Boot 文章目录Spring Boot1. 概述1.1 由来1.2 特征2. 入门程序2.1 通过maven工程方式创建2.2 直接通过IDEA创建3. SpringBoot配置3.1 概述3.2 Spring Boot的配置文件3.2.1 yml格式配置文件3.2.2 多配置切换3.3 Spring Boot属性注入3.3.1 @Value注入3.3.2 @ConfigurationProperties注入4. Spring Boot整合SpringMVC4.1 整合JSP4.2 整合Sp

Satellite_H的博客 1587
上一篇: 玩转 Spring Boot 原理篇(内嵌Tomcat实现原理&优雅停机源码剖析)
下一篇: 玩转 Spring Boot 应用篇(解决菜菜店铺商品超卖问题)
一猿小讲
博客等级 码龄15年 294粉丝 156原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值