瑞吉外卖笔记

本文涉及的产品
云原生内存数据库 Tair,内存型 2GB
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Redis 版,经济版 1GB 1个月
简介: 这是一份写给自己的笔记,主要记录瑞吉外卖项目中自己没有了解过的知识点。我将按照功能来分别解析

前言


这是一份写给自己的笔记,主要记录瑞吉外卖项目中自己没有了解过的知识点。我将按照功能来分别解析


项目结构


除了基本的Controller,Service,Mapper,Config,Util,Entity层之外,还有我之前没了解过的common层和dto层


common层包含整个应用程序使用的公共辅助方法,和util层类似,但更有普适性,common层类中的方法对绝大多数功能都有辅助作用,如R类的作用是返回给前端通用格式的结果,能简化整个controller类的开发


dto层,DTO全称为Data Transfer Object,数据传输对象,起到数据封装与隔离的作用


common层工具


返回结果类R


此类是一个通用结果类,服务端响应的所有结果最终都会包装成此种类型返回给前端页面


import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/**
 * 通用返回结果,服务端响应的数据最终都会封装成此对象
 * @param <T>
 */
@Data
public class R<T> {
    private Integer code; //编码:1成功,0和其它数字为失败
    private String msg; //错误信息
    private T data; //数据
    private Map map = new HashMap(); //动态数据
    public static <T> R<T> success(T object) {
        R<T> r = new R<T>();
        r.data = object;
        r.code = 1;
        return r;
    }
    public static <T> R<T> error(String msg) {
        R r = new R();
        r.msg = msg;
        r.code = 0;
        return r;
    }
    public R<T> add(String key, Object value) {
        this.map.put(key, value);
        return this;
    }
}
//A. 如果业务执行结果为成功, 构建R对象时, 只需要调用 success 方法; 如果需要返回数据传递 object 参数, 如果无需返回, 可以直接传递null。
//B. 如果业务执行结果为失败, 构建R对象时, 只需要调用error 方法, 传递错误提示信息即可。


业务逻辑


新增信息


以前的项目中,我在Controller方法中设置形参都是把一个个字段设置为形参,然后在DAO层分别写每个字段的添加方法,这样十分麻烦,我们可以把接收的形参设置为JSON实体类对象,再由mabatis-plus自动生成sql语句,就能省很多事了


public R<AddressBook> save(@RequestBody AddressBook addressBook) {}

菜品分页查询


在前端回显数据时,我们要回显菜品分类的名称,但是菜品名称不在Dish这张表中,因此Dish实体类中也没有菜品名称的字段,因此我们用DishDto来扩展Dish,来辅助后端给前端发数据


  @Data
public class DishDto extends Dish {
    private List<DishFlavor> flavors = new ArrayList<>();
    private String categoryName; //菜品分类名称
    private Integer copies;
}
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
    //构造分页构造器对象
    Page<Dish> pageInfo = new Page<>(page,pageSize);
    Page<DishDto> dishDtoPage = new Page<>();
    //条件构造器
    LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
    //添加过滤条件
    queryWrapper.like(name != null,Dish::getName,name);
    //添加排序条件
    queryWrapper.orderByDesc(Dish::getUpdateTime);
    //执行分页查询
    dishService.page(pageInfo,queryWrapper);//查询后的数据存在pageInfo里
    //对象拷贝
    BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");//records属性就是分页构造对象查询的分页数据,我们拷贝一份对象给新的分页构造器,但是我们要忽略records这个属性,因为我们要自己构建新的分页数据,也就是把菜品名称查出来并放到页面构造器中
    List<Dish> records = pageInfo.getRecords();
    List<DishDto> list = records.stream().map((item) -> {//stream流遍历并构建新的list
        DishDto dishDto = new DishDto();
        BeanUtils.copyProperties(item,dishDto);
        Long categoryId = item.getCategoryId();//分类id
        //根据id查询分类对象
        Category category = categoryService.getById(categoryId);
        if(category != null){
            String categoryName = category.getName();
            dishDto.setCategoryName(categoryName);
        }
        return dishDto;//每修改完一个新元素都要返回该元素,然后才能由collect()方法整合成新列表
    }).collect(Collectors.toList());
    dishDtoPage.setRecords(list);//给新构造器设置我们自己修改过的分页数据
    return R.success(dishDtoPage);
}

其他技术要点


前端知识


localStorage和SessionStorage的区别:


localStorage 和 sessionStorage 属性允许在浏览器中存储 key/value 对的数据。


localStorage 用于长久保存整个网站的数据,保存的数据没有过期时间,直到手动去删除。


localStorage 属性是只读的。


如果你只想将数据保存在当前会话中,可以使用 sessionStorage 属性, 该数据对象临时保存同一窗口(或标签页)的数据,在关闭窗口或标签页之后将会删除这些数据。


JS对长整型数据进行处理时会损失精度


因此当我们数据库中id为长整型时,把id交给前端之前,先把id转成String类型,这样就不会损失精度了


java基础


Lambda表达式


Lambda表达式超详细总结Code0cean的博客-CSDN博客lambda表达式详细总结


Lambda表达式简化了函数式接口(只有一个抽象方法的接口)的实现操作,用Lambda表达式可以很快地创建函数式接口的实现对象。


当要传递给Lambda体的操作,已经有实现的方法了,就可以使用方法引用


Comparator<Integer> comparable=(x,y)->Integer.compare(x,y);
Comparator<Integer> integerComparable=Integer::compare;//使用方法引用实现相同效果


Stream 流


Java 8 Stream | 菜鸟教程 (runoob.com)


Java8 Stream:2万字20个实例,玩转集合的筛选、归约、分组、聚合云深i不知处的博客-CSDN博客java stream 分组聚合


Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。


Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。


Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。


这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。


元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。


全局异常处理


使用全局异常处理可以避免重复在每一个业务逻辑里面写try...catch来捕获异常


/**
 * 全局异常处理
 在项目中自定义一个全局异常处理器,在异常处理器上加上注解 @ControllerAdvice,可以通过属性annotations指定拦截哪一类的Controller方法。 并在异常处理器的方法上加上注解 @ExceptionHandler 来指定拦截的是那一类型的异常。
 */
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody//用这个注解可以将返回值R对象以JSON格式响应给页面
@Slf4j
public class  GlobalExceptionHandler {
    /**
     * 异常处理方法
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)//声明拦截异常的类型
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
        log.error(ex.getMessage());
        if(ex.getMessage().contains("Duplicate entry")){
            String[] split = ex.getMessage().split(" ");
            String msg = split[2] + "已存在";
            return R.error(msg);
        }
        return R.error("未知错误");
    }
}


自定义业务异常类


我们可以通过自定义业务异常类来抛出自定义的信息


/**
 * 自定义业务异常类
 */
public class CustomException extends RuntimeException {
    public CustomException(String message){
        super(message);
    }
}

使用时代码如下


throw new CustomException("当前分类下关联了套餐,不能删除");//已经关联套餐,抛出一个业务异常


在全局异常处理器中捕获自定义异常


@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex){
    log.error(ex.getMessage());
    return R.error(ex.getMessage());
}

Serliazeable


在Redis中存储对象,该对象是需要被序列化的,而对象要想被成功的序列化,就必须得实现 Serializable 接口.Java 序列化技术可以使你将一个对象的状态写入一个Byte 流里(系列化),并且可以从其它地方把该Byte 流里的数据读出来(反序列化)


只要让类继承Serialzable接口就可以实现序列化


public class R<T> implements Serializable{

Spring 基础


接收前端发来的JSON数据


//接收前端发来的JSON数据,需要在相应形参前加@RequestBody注解,因为JSON数据在请求体中被发过来
public R<Employee> login(HttpServletRequest request,@RequestBody Employee employee){}

Springboot中 json序列化与反序列化


Springboot集成并封装了Jackson,使用Jackson来操作json,JSON的序列化与反序列化我们可以通过@Responsebody@RequestBody轻松实现


Lombok


Lombok是一个可以减少java模板代码的工具,我们可以用Lombok来简化Entity类的代码,省去了get,set及构造方法,接下来介绍lombok的常用注解


@Data


在Entity类中使用该注解,在项目编译时,会帮我们自动加上set,get以及toString方法


@Slf4j


在类上使用该注解,我们可以在类中的方法使用log函数来输出日志信息


MybatisPlus


条件构造器


顾名思义,作用就是封装查询条件,生成sql的where条件


在项目中,查询数据库用到了LambdaQuaryWrapper


 //2、根据页面提交的用户名username查询数据库
    LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(Employee::getUsername,employee.getUsername());//这里通过Lambda表达式来获取User实体类中username的字段名,省去了查数据库的步骤,这就是lambda条件构造器的优点
    Employee emp = employeeService.getOne(queryWrapper);//相当于里面放了一个查询语句,通过查询语句获取结果


公共字段填充


数据库里经常需要填充一些公共字段,如用户id,更新时间等,我们可以把这些操作交给mybatis-plus自动完成


第一步去实体类给要自动填充的字段加注解


 

   //通过@tablefield声明要自动填充的注解,并指定填充策略
    @TableField(fill = FieldFill.INSERT) //插入时填充字段
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段
    private LocalDateTime updateTime;
    @TableField(fill = FieldFill.INSERT) //插入时填充字段
    private Long createUser;
    @TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段
    private Long updateUser;


第二步在common层添加自定义元数据对象处理器


@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
    /**
     * 插入操作,自动填充
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {//实现插入和更新对应的方法
        log.info("公共字段自动填充[insert]...");
        log.info(metaObject.toString());
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime",LocalDateTime.now());
        metaObject.setValue("createUser",BaseContext.getCurrentId());
        metaObject.setValue("updateUser",BaseContext.getCurrentId());
    }
    /**
     * 更新操作,自动填充
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("公共字段自动填充[update]...");
        log.info(metaObject.toString());
        long id = Thread.currentThread().getId();
        log.info("线程id为:{}",id);
        metaObject.setValue("updateTime",LocalDateTime.now());
        metaObject.setValue("updateUser",BaseContext.getCurrentId());
    }
}

但是在MyMetaObjectHandler中,我们不能直接获取HttpSession对象,也就不能直接获取session中的用户ID,此项目采用ThreadLocal解决


ThreadLocal


ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。


ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问当前线程对应的值。


我们可以在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id),然后在MyMetaObjectHandler的updateFill方法中调用ThreadLocal的get方法来获得当前线程所对应的线程局部变量的值(用户id)。 如果在后续的操作中, 我们需要在Controller / Service中要使用当前登录用户的ID, 可以直接从ThreadLocal直接获取。


/**
 * 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
 */
public class BaseContext {
    private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
    /**
     * 设置值
     * @param id
     */
    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }
    /**
     * 获取值
     * @return
     */
    public static Long getCurrentId(){
        return threadLocal.get();
    }
}

然后我们在filter中判断用户的登录情况,放行前给ThreadLocal赋值


Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);


然后我们就可以在各个地方获取ThreadLocal变量了


项目优化


SpringCache


Spring Cache只是提供了一层抽象,底层可以切换不同的cache实现。具体就是通过CacheManager接口来统一不同的缓存技术。CacheManager是Spring提供的各种缓存技术抽象接口。


针对不同的缓存技术需要实现不同的CacheManager:

CacheManager

描述

EhCacheCacheManager

使用EhCache作为缓存技术

GuavaCacheManager

使用Google的GuavaCache作为缓存技术

RedisCacheManager

使用Redis作为缓存技术


注解


在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个:

注解

说明

@EnableCaching

开启缓存注解功能

@Cacheable

在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中

@CachePut

将方法的返回值放到缓存中

@CacheEvict

将一条或多条数据从缓存中删除


在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。


例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。


@CachePut注解


@CachePut 说明:


作用: 将方法返回值,放入缓存


value: 缓存的名称, 每个缓存名称下面可以有很多key


key: 缓存的key  ----------> 支持Spring的表达式语言SPEL语法


在save方法上加注解@CachePut


当前UserController的save方法是用来保存用户信息的,我们希望在该用户信息保存到数据库的同时,也往缓存中缓存一份数据,我们可以在save方法上加上注解 @CachePut,用法如下:


/**
* CachePut:将方法返回值放入缓存
* value:缓存的名称,每个缓存名称下面可以有多个key
* key:缓存的key
*/
@CachePut(value = "userCache", key = "#user.id")
@PostMapping
public User save(User user){
    userService.save(user);
    return user;
}

key的写法如下:


#user.id : #user指的是方法形参的名称, id指的是user的id属性 , 也就是使用user的id属性作为key ;


#user.name: #user指的是方法形参的名称, name指的是user的name属性 ,也就是使用user的name属性作为key ;


@CacheEvict注解


@CacheEvict 说明:


作用: 清理指定缓存


value: 缓存的名称,每个缓存名称下面可以有多个key


key: 缓存的key  ----------> 支持Spring的表达式语言SPEL语法


在 delete 方法上加注解@CacheEvict


当我们在删除数据库user表的数据的时候,我们需要删除缓存中对应的数据,此时就可以使用@CacheEvict注解, 具体的使用方式如下:


/**
* CacheEvict:清理指定缓存
* value:缓存的名称,每个缓存名称下面可以有多个key
* key:缓存的key
*/
@CacheEvict(value = "userCache",key = "#p0")  //#p0 代表第一个参数
//@CacheEvict(value = "userCache",key = "#root.args[0]") //#root.args[0] 代表第一个参数
//@CacheEvict(value = "userCache",key = "#id") //#id 代表变量名为id的参数
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id){
    userService.removeById(id);
}


在更新数据的时候也需要删除缓存,以免数据不同步


@Cacheable注解


@Cacheable 说明:


作用: 在方法执行前,spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中


value: 缓存的名称,每个缓存名称下面可以有多个key


key: 缓存的key  ----------> 支持Spring的表达式语言SPEL语法


在getById上加注解@Cacheable


/**
* Cacheable:在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中
* value:缓存的名称,每个缓存名称下面可以有多个key
* key:缓存的key
*/
@Cacheable(value = "userCache",key = "#id")
@GetMapping("/{id}")
public User getById(@PathVariable Long id){
    User user = userService.getById(id);
    return user;
}


缓存非null值


在@Cacheable注解中,提供了两个属性分别为: condition, unless 。


condition : 表示满足什么条件, 再进行缓存 ;


unless : 表示满足条件则不缓存 ; 与上述的condition是反向的 ;


具体实现方式如下:


/**
 * Cacheable:在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中
 * value:缓存的名称,每个缓存名称下面可以有多个key
 * key:缓存的key
 * condition:条件,满足条件时才缓存数据
 * unless:满足条件则不缓存
 */
@Cacheable(value = "userCache",key = "#id", unless = "#result == null")
@GetMapping("/{id}")
public User getById(@PathVariable Long id){
    User user = userService.getById(id);
    return user;
}


注意: 此处,我们使用的时候只能够使用 unless, 因为在condition中,我们是无法获取到结果 #result的。


MySQL主从数据库


面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。 对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。 ——瑞吉外卖PPT


主从复制


以下内容摘自CSDN博客 要不一起ci个饭的博客-CSDN博客_主从复制


主从复制的定义


主从复制,是用来建立一个和主数据库完全一样的数据库环境,称为从数据库。在赋值过程中,一个服务器充当主服务器,而另外一台服务器充当从服务器。
当一台从服务器连接到主服务器时,从服务器会通知主服务器从服务器的日志文件中读取最后一次成功更新的位置。然后从服务器会接收从哪个时刻起发生的任何更新,然后锁住并等到主服务器通知新的更新


做主从复制的好处


做数据的热备
作为后备数据库,主数据库服务器故障后,可切换到从数据库继续工作,避免数据丢失。


架构的扩展
业务量越来越大,I/O访问频率过高,单机无法满足,此时做多库的存储,降低磁盘I/O访问的评率,提高单个机器的I/O性能。


读写分离(重点)
使数据库能支持更大的并发。在报表中尤其重要。由于部分报表sql语句非常的慢,导致锁表,影响前台服务。如果前台使用master,报表使用slave,那么报表sql将不会造成前台锁,保证了前台速度。


读写分离


面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。 对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。


ShardingJDBC介绍


Sharding-JDBC定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。


使用Sharding-JDBC可以在程序中轻松的实现数据库读写分离。


Sharding-JDBC具有以下几个特点:


1). 适用于任何基于JDBC的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。


2). 支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。


3). 支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer,PostgreSQL以及任何遵循SQL92标准的数据库。


Nginx,Swagger


重构项目的时候会详细学一下,目前看这些都是纸上谈兵。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
4天前
|
缓存 JavaScript Java
苍穹外卖开发心得(下)
苍穹外卖开发心得(下)
15 5
|
4天前
|
前端开发 Java API
苍穹外卖开发心得(上)
苍穹外卖开发心得(上)
20 4
|
5天前
|
存储 SQL 前端开发
瑞吉外卖精华部分总结(1)
瑞吉外卖精华部分总结(1)
12 0
|
24天前
|
运维 前端开发 测试技术
瑞吉外卖业务开发(1)
瑞吉外卖业务开发
19 3
|
24天前
|
JSON 前端开发 Java
瑞吉外卖业务开发(2)
瑞吉外卖业务开发
23 3
|
24天前
|
JSON 前端开发 安全
瑞吉外卖业务开发(4)
瑞吉外卖业务开发
15 2
|
24天前
|
存储 JSON 前端开发
瑞吉外卖业务开发(3)
瑞吉外卖业务开发
22 1
|
26天前
|
供应链 安全 数据挖掘
外卖跑腿系统开发详情丨校园外卖跑腿系统开发指南
开发外卖跑腿系统旨在服务于外卖平台和跑腿服务商,实现用户下单、骑手接单及订单管理等功能。系统包括用户端应用(注册、下单、支付等)、商家管理(菜单更新、订单处理)、骑手端应用(任务接收、配送)以及实时订单管理。此外,系统支持多种支付方式、订单结算、评价反馈机制、数据统计报表和客户服务,确保交易安全、提升效率并优化用户体验。
|
3天前
|
SQL JSON 算法
技术经验解读:元旦三天假期,实现一个电商退单管理系统【三】
技术经验解读:元旦三天假期,实现一个电商退单管理系统【三】
|
2月前
|
安全 Java 数据库连接
首次面试经历(忘指导)当我在简历上写了苍穹外卖,瑞吉外卖时……
首次面试经历(忘指导)当我在简历上写了苍穹外卖,瑞吉外卖时……
703 1