小柒味来 - 项目开发总结与经验归纳

一、前言

本文把项目中常用的中间件、框架与技术点做一个汇总:包括 Nginx、JWT、Redis、Spring Cache、AOP、事务、定时任务、WebSocket、OSS、POI 等


二、技术点

每个条目格式:概念、场景、实现要点 / 快速示例。

1. Nginx

专门写了一篇,请移至多功能服务器 - Nginx

  • 概念:通过调度算法把请求分发到多台应用服务器,实现水平扩展与容灾。
  • 场景:流量分发、静态资源加速、反向代理
  • 要点:常用算法:轮询、最小连接、IP hash。配合 health check 与 SSL。

1.1 正向代理

  • 概念:客户端通过代理访问外网。

1.2 反向代理

  • 概念:对外表现为单一服务器,内部转发到后端服务

2. MD5 加密

  • 特点:固定长度、不可逆

注意单纯 MD5 不够安全,一定要配合盐值等加强的算法。

  • 示例:
private static final String SALT = "qi";
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());

3. Swagger

Swagger的详细介绍在接口文档生成 - Swagger

  • 作用:自动生成在线 API 文档。

  • 集成要点:

    1. 引入 Knife4j(swagger的ui增强版)依赖
    <dependency>
                <groupId>com.github.xiaoymin</groupId>
                <artifactId>knife4j-spring-boot-starter</artifactId>
            </dependency>
    
    1. 在配置类中添加配置
    @Bean
        public Docket docket1(){
            log.info("准备生成接口文档...");
            ApiInfo apiInfo = new ApiInfoBuilder()
                    .title("小柒味来项目接口文档")
                    .version("2.0")
                    .description("小柒味来项目接口文档")
                    .build();
    
            Docket docket = new Docket(DocumentationType.SWAGGER_2)
                    .groupName("管理端接口")
                    .apiInfo(apiInfo)
                    .select()
                    //指定生成接口需要扫描的包
                    .apis(RequestHandlerSelectors.basePackage("com.qi.controller.admin"))
                    .paths(PathSelectors.any())
                    .build();
    
            return docket;
        }
    
    1. 使用注解标注接口:
    @Api(tags = "用户模块")
    @ApiModel
    @ApiModelProperty
    @ApiOperation("查询用户")
    
    1. Knife4j的访问地址为:http://localhost:8080/doc.html

4. JWT

  • 组成:Header(类型+算法) + Payload(载荷) + Signature(签名)。
  • 场景:身份认证、授权、服务间信息传递(无状态)。
  • 实现要点:
    1. 引入 JWT 库;
    2. 封装工具类负责生成或解析 token;
    3. 在拦截器或过滤器中验证 token 并从载荷取用户信息。

5. 拦截器 Interceptor

  • 概念:基于 Spring 的请求拦截(本质与 AOP 相关)。

  • 场景:登录校验、权限控制、性能监控、请求日志。

  • 实现:

    1. 实现 HandlerInterceptor,重写 preHandle、postHandle、afterCompletion;
    @Component
    @Slf4j
    public class JwtTokenUserInterceptor implements HandlerInterceptor {
    
        @Autowired
        private JwtProperties jwtProperties;
    
        /**
         * 校验jwt
         *
         * @param request
         * @param response
         * @param handler
         * @return
         * @throws Exception
         */
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            //判断当前拦截到的是Controller的方法还是其他资源
            if (!(handler instanceof HandlerMethod)) {
                //当前拦截到的不是动态方法,直接放行
                return true;
            }
    
            //1、从请求头中获取令牌
            String token = request.getHeader(jwtProperties.getUserTokenName());
    
            //2、校验令牌
            try {
                log.info("jwt校验:{}", token);
                Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
                Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
                log.info("当前用户id:", userId);
                BaseContext.setCurrentId(userId);
                //3、通过,放行
                return true;
            } catch (Exception ex) {
                //4、不通过,响应401状态码
                response.setStatus(401);
                return false;
            }
        }
    }
    
    1. 在配置类中注册并设置拦截路径。
        @Autowired
        private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
        /**
         * 注册自定义拦截器
         *
         * @param registry
         */
        protected void addInterceptors(InterceptorRegistry registry) {
            log.info("开始注册自定义拦截器...");
            registry.addInterceptor(jwtTokenAdminInterceptor)
                    .addPathPatterns("/admin/**")
                    .excludePathPatterns("/admin/employee/login");
        }
    
    1. preHandle、postHandle、afterCompletion详解
    方法名 调用时机 功能 典型用途 返回值
    preHandle Controller 方法执行前 权限校验、日志记录、请求参数处理 登录验证、Token 校验、请求限流 返回 true 放行,返回 false 拦截请求
    postHandle Controller 方法执行后,视图渲染前 调整返回数据或视图 统一响应包装、添加公共数据 无
    afterCompletion 整个请求处理完成后(视图渲染结束) 清理资源、记录日志、处理异常 性能监控、日志记录、异常处理 无返回值,即使 preHandle 返回 false 也可能调用

7. 异常处理 — 全局异常处理器

  • 注解:@RestControllerAdvice + @ExceptionHandler。

  • 作用:统一捕获异常、返回规范化错误响应、避免暴露堆栈信息。

  • 实例

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 捕获业务异常
     *
     * @param ex
     * @return
     */
    @ExceptionHandler
    public Result exceptionHandler(BaseException ex) {
        log.error("异常信息:{}", ex.getMessage());
        return Result.error(ex.getMessage());
    }

    /*
     * 捕获SQL异常
     * */
    @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) {
        String message = ex.getMessage();
        log.error("异常信息:{}", message);
        if (message.contains("Duplicate entry")) {
            String[] split = message.split(" ");
            String msg = split[2] + MessageConstant.ALREADY_EXISTS;
            return Result.error(msg);

        } else {
            return Result.error(MessageConstant.UNKNOWN_ERROR);
        }

    }
}

8. AOP 切面

  • 概念:把横切关注点(如日志、权限、事务)从业务中抽离。

  • 使用场景:操作日志、权限校验、性能统计、事务增强等。

  • 实现步骤:

    1. 定义自定义注解
    /*
    * 自定义注解,用于标记和记录方法的执行信息
    * */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AutoFill {
        //数据库操作类型:INSERT、UPDATE
        OperationType value();
    }
    
    1. 编写带 @Aspect 的切面类,定义通知类型(@Before / @After / @Around)
    @Aspect
    @Component
    @Slf4j
    public class AutoFillAspect {
        /**
         * 切入点方法
         * 定义了需要进行自动填充的切入点
         * 该切入点匹配使用了@AutoFill注解的Mapper接口中的所有方法
         */
        @Pointcut("@annotation(com.qi.annotation.AutoFill) && execution(* com.qi.mapper.*.*(..))")
        public void autoFillPointCut() {
        }
    
        /**
         * 在执行切入点方法之前进行自动填充
         *
         * @param joinPoint 切入点对象,包含执行方法的信息
         */
        @Before("autoFillPointCut()")
        public void autoFill(JoinPoint joinPoint) {
            // 记录自动填充开始的日志
            log.info("开始进行公共字段自动填充。。。。");
    
            // 获取方法签名和@AutoFill注解
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
    
            // 获取操作类型(插入或更新)
            OperationType operationType = autoFill.value();
    
            // 获取方法参数
            Object[] args = joinPoint.getArgs();
            if (args == null || args.length == 0) {
                return;
            }
            Object entity = args[0];
    
            // 获取当前时间和用户ID
            LocalDateTime now = LocalDateTime.now();
            Long currentId = BaseContext.getCurrentId();
    
            // 根据操作类型进行相应的字段填充
            if (operationType == OperationType.INSERT) {
                try {
                    // 对于插入操作,填充创建时间和用户以及更新时间和用户
                    Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                    Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                    Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                    Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
    
                    setCreateTime.invoke(entity, now);
                    setCreateUser.invoke(entity, currentId);
                    setUpdateTime.invoke(entity, now);
                    setUpdateUser.invoke(entity, currentId);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            } else if (operationType == OperationType.UPDATE) {
                try {
                    // 对于更新操作,仅填充更新时间和用户
                    Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                    Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
                    setUpdateTime.invoke(entity, now);
                    setUpdateUser.invoke(entity, currentId);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
    
    1. 在目标方法/类上加注解
        @AutoFill(value = OperationType.INSERT)
        void insert(Dish dishDTO);
    

9. Redis

详细使用请移步存储中间件-Redis

  • 概念:基于内存的 key-value 非关系型数据库。

  • 场景:缓存、消息队列、排行榜、分布式锁等。

  • 接入步骤:

    1. 引入依赖(spring-boot-starter-data-redis)
    <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    1. 配置 Redis 连接
    1. 定义与注入 RedisTemplate

      • 自定义序列化
      @Bean
          public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
              // 记录创建RedisTemplate对象的日志
              log.info("开始创建redis对象");
              // 实例化RedisTemplate
              RedisTemplate redisTemplate = new RedisTemplate();
              // 设置Redis连接工厂
              redisTemplate.setConnectionFactory(redisConnectionFactory);
              // 设置键的序列化方式为StringRedisSerializer,确保键以字符串形式存储
              redisTemplate.setKeySerializer(new StringRedisSerializer());
              // 返回配置好的RedisTemplate实例
              return redisTemplate;
          }
      
      • 注入
          @Autowired
          private RedisTemplate redisTemplate;
      
          @Test
          public void testRedisTemplate(){
              System.out.println(redisTemplate);
              //string数据操作
              ValueOperations valueOperations = redisTemplate.opsForValue();
              //hash类型的数据操作
              HashOperations hashOperations = redisTemplate.opsForHash();
              //list类型的数据操作
              ListOperations listOperations = redisTemplate.opsForList();
              //set类型数据操作
              SetOperations setOperations = redisTemplate.opsForSet();
              //zset类型数据操作
              ZSetOperations zSetOperations = redisTemplate.opsForZSet();
          }
      

10. Redis 序列化方式

  • JdkSerializationRedisSerializer:Java 默认(体积大)。
  • StringRedisSerializer:字符串序列化。
  • GenericToStringSerializer:泛型转字符串。
  • Jackson2JsonRedisSerializer / GenericJackson2JsonRedisSerializer:JSON 序列化(常用,兼容性好)

11. Spring Cache

  • 概念:通过注解简化缓存操作。

  • 常用注解:

    • @EnableCaching(启动类)
    • @Cacheable(读取并缓存)
    • @CachePut(更新缓存)
    • @CacheEvict(清理缓存)
  • 流程:

    1. 引入缓存实现(如 Redis)
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    
    1. 开启缓存
    /**
     * 1、开启基于注解的缓存 @EnableCaching
     */
    @SpringBootApplication
    @EnableCaching  //开启缓存
    public class CacheApplication{
        public static void main(String[] args) {
            SpringApplication.run(CacheApplication.class, args);
        }
    
    }
    
    1. 标注方法
    @Cacheable(value = "emp" ,key = "targetClass + methodName +#p0")
    public Employee getEmp(Integer id){
        System.out.println("查询"+id+"号员工");
        Employee emp = employeeMapper.getEmpById(id);
        return emp;
    }
    

12. HttpClient

  • 作用:发送 HTTP 请求并接收响应(服务间调用、第三方 API)

  • 官方示例:Http Client 快速入门

  • 流程:

    1. 创建 HttpClient;
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
    
    1. 构造请求(GET/POST);
        try{
                URIBuilder builder = new URIBuilder(url);
                if(paramMap != null){
                    for (String key : paramMap.keySet()) {
                        builder.addParameter(key,paramMap.get(key));
                    }
                }
                URI uri = builder.build();	
        //创建GET请求
        HttpGet httpGet = new HttpGet(uri);
        //发送请求
        response = httpClient.execute(httpGet);
    
    1. 执行并处理响应,最后释放连接。
        //判断响应状态
                if (response.getStatusLine().getStatusCode() == 200) {
                    result = EntityUtils.toString(response.getEntity(), "UTF-8");
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    response.close();
                    httpClient.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    

13. 微信小程序

内容较多,请移步小柒味来 - 微信登录与商品浏览功能开发查看流程

  • 要点:使用微信官方开发文档(登录、会话、API 权限)。
  • 登录文档:微信小程序登录流程(wx.login、后端换取session_key)等。

14. Spring Task

  • 场景:订单超时处理、定时推送等。

  • 流程:

    1. 启动类启用注解@EnableScheduling

    2. 给需要定时的任务加上注解@Scheduled(cron = "0/5 * * * * ?")

  • 注意:长任务考虑异步或分布式调度,cron 表达式要谨慎测试


15. WebSocket(实时双向通信)

  • 概念:基于 TCP 的全双工通信,适合实时推送(订单通知、催单)。

  • 流程:

    1. 后端建立 WebSocket 服务器端点;
    2. 前端(小程序/网页)建立连接并处理消息。

16. 阿里云 OSS

  • 用途:存储图片、视频、文件等静态资源。

  • 步骤:

    1. 引入OSS SDK依赖
    <dependency>
        <groupId>com.aliyun.oss</groupId>
        <artifactId>aliyun-sdk-oss</artifactId>
    </dependency>
    
    1. 在配置文件(YAML)写入 endpoint、accessKeyId、accessKeySecret、bucketName
      alioss:
        endpoint: ${qi.alioss.endpoint}
        access-key-id: ${qi.alioss.access-key-id}
        access-key-secret: ${qi.alioss.access-key-secret}
        bucket-name: ${qi.alioss.bucket-name}
    
    1. 编写上传工具类(参考快速使用 OSS SDK - 对象存储 OSS - 阿里云)并处理文件流
    @Data
    @AllArgsConstructor
    @Slf4j
    public class AliOssUtil {
    
        private String endpoint;
        private String accessKeyId;
        private String accessKeySecret;
        private String bucketName;
    
        /**
         * 文件上传
         *
         * @param bytes
         * @param objectName
         * @return
         */
        public String upload(byte[] bytes, String objectName) {
    
            // 创建OSSClient实例。
            OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
    
            try {
                // 创建PutObject请求。
                ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
            } catch (OSSException oe) {
                System.out.println("Caught an OSSException, which means your request made it to OSS, "
                        + "but was rejected with an error response for some reason.");
                System.out.println("Error Message:" + oe.getErrorMessage());
                System.out.println("Error Code:" + oe.getErrorCode());
                System.out.println("Request ID:" + oe.getRequestId());
                System.out.println("Host ID:" + oe.getHostId());
            } catch (ClientException ce) {
                System.out.println("Caught an ClientException, which means the client encountered "
                        + "a serious internal problem while trying to communicate with OSS, "
                        + "such as not being able to access the network.");
                System.out.println("Error Message:" + ce.getMessage());
            } finally {
                if (ossClient != null) {
                    ossClient.shutdown();
                }
            }
    
            //文件访问路径规则 https://BucketName.Endpoint/ObjectName
            StringBuilder stringBuilder = new StringBuilder("https://");
            stringBuilder
                    .append(bucketName)
                    .append(".")
                    .append(endpoint)
                    .append("/")
                    .append(objectName);
    
            log.info("文件上传到:{}", stringBuilder.toString());
    
            return stringBuilder.toString();
        }
    }
    

    详细请看小柒味来 - 菜品管理模块开发中的文件上传部分


17. 事务处理

  • 概念:保持一组操作的原子性(ACID)。
  • 注解:@Transactional(常用属性:rollbackFor、propagation)
  • 传播行为:REQUIRED(默认)、REQUIRES_NEW 等,设计时注意嵌套调用的事务边界。

18. Apache ECharts

  • 用途:前端数据可视化(图表、报表)
  • 接入:前端引入 ECharts,通过接口拿数据并渲染图表。
  • 快速上手

19. Apache POI

  • 用途:生成/读取 Excel 报表

  • 快速流程:

    1. new XSSFWorkbook();
    2. 导入模板文件并进行填充
    3. setCellValue();写出并关闭流。
      参考:POI 官方 QuickGuide
    @Override
        public void exportBusinessData(HttpServletResponse httpServletResponse) {
            //查询数据库,获取营业数据--查询最近30天的运营数据
            LocalDate dateBegin = LocalDate.now().minusDays(30);
            LocalDate dateEnd = LocalDate.now().minusDays(1);
    
            //查询概览数据
            BusinessDataVO businessDataVO = workspaceService.getBusinessData(LocalDateTime.of(dateBegin, LocalTime.MIN), LocalDateTime.of(dateEnd, LocalTime.MAX));
    
            //通过POI将数据写入到Excel中
            InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");
    
            try {
                //基于模板创建一个新的Excel文件
                XSSFWorkbook excel = new XSSFWorkbook(in);
    
                //获取表格文件的Sheet页
                XSSFSheet sheet1 = excel.getSheet("Sheet1");
    
                //填充时间数据
                sheet1.getRow(1).getCell(1).setCellValue("时间:" + dateBegin + "至" + dateEnd);
    
                //获取第四行
                XSSFRow row = sheet1.getRow(3);
                row.getCell(2).setCellValue(businessDataVO.getTurnover());
                row.getCell(4).setCellValue(businessDataVO.getOrderCompletionRate());
                row.getCell(6).setCellValue(businessDataVO.getNewUsers());
    
                //获得第五行VO
                row = sheet1.getRow(4);
                row.getCell(2).setCellValue(businessDataVO.getValidOrderCount());
                row.getCell(4).setCellValue(businessDataVO.getUnitPrice()
    
                //填充明细数据
                for (int i = 0; i < 30; i++) {
                    LocalDate date = dateBegin.plusDays(i);
                    //查询某一天的营业数据
                    BusinessDataVO businessData = workspaceService.getBusinessData(LocalDateTime.of(date, LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX));
    
                    //获得某一行
                    row = sheet1.getRow(7 + i);
                    row.getCell(1).setCellValue(date.toString());
                    row.getCell(2).setCellValue(businessData.getTurnover());
                    row.getCell(3).setCellValue(businessData.getValidOrderCount());
                    row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
                    row.getCell(5).setCellValue(businessData.getUnitPrice());
                    row.getCell(6).setCellValue(businessData.getNewUsers());
                }
    
                //3. 通过输出流将Excel文件下载到客户端浏览器
                ServletOutputStream out = httpServletResponse.getOutputStream();
                excel.write(out);
    
                //关闭资源
                out.close();
                excel.close();
    
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    

20. 排除依赖

  • 用途:避免传递依赖冲突或不需要的依赖引入。
<dependency>
  <groupId>some</groupId>
  <artifactId>lib</artifactId>
  <exclusions>
    <exclusion>
      <groupId>bad</groupId>
      <artifactId>conflict</artifactId>
    </exclusion>
  </exclusions>
</dependency>

21. @ConfigurationProperties

  • 用途:把 YAML/Properties 的配置批量注入到 Bean。
  • 优点:比 @Value 更适合批量/数组配置,支持 prefix 映射。
  • 使用:@ConfigurationProperties(prefix="xxx") + @EnableConfigurationProperties 或 @Component
@Component
@ConfigurationProperties(prefix = "qi.alioss")
@Data
public class AliOssProperties {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;
}

三、工作流程图

  1. 业务功能总览

四、具体业务流程图(按流程列出并留图位)

  1. 拦截器(请求进入 -> 拦截器校验 -> 放行/抛异常)
  2. 登录(前端登录 -> 后端校验 -> 返回 JWT)
  3. 公共字段填充(请求到达 -> AOP 填充公共字段如 createTime/createBy)
  4. 新增
  5. 店铺营业状态
  6. 微信小程序登录
  7. 用户端查看菜品 — Redis 缓存
  8. 用户查看套餐 — Spring Cache 注解缓存
  9. 添加菜品/套餐至购物车
  10. 用户下单
  11. 用户支付(第三方支付回调处理,幂等与订单状态更新)
  12. 订单状态定时修改 — Spring Task(超时未支付自动关闭)
  13. 来单/催单 — WebSocket
  14. 导出报表 — POI

项目中用到的大部分知识点就已经整理完了,在项目中遇到的小问题整理请看:小柒味来 - 零碎知识点学习记录


欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1701220998@qq.com
导航页 GitHub