微服务简介

微服务是一种结构风格,代表着一种通过将应用程序拆分成小型、独立的功能模块(服务)的开发方式。

每个模块实现独立的业务功能不限语言,不限技术,每个服务之间通过轻量级通信机制(例如HTTP/REST或消息队列)进行交互。

核心思想:解耦应用程序,提升灵活性和维护性

优点:1.模块独立解耦 2.独立部署、快速迭代(某个模块改了某个模块重新部署,不需要像单体程序那样全部重新部署)3.灵活的技术栈 4.高扩展性 5.容错性好

缺点:分布式系统的复杂性(服务器成本增加,开发人员的成本,运维成本增加)

前置知识

MybatisPlus

使用MyBatisPlus的基本步骤

(1)引入MyBatisPlus依赖

首先pom文件中导入依赖

<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
  <version>3.5.7</version>
</dependency>

该依赖中已经包含了MyBatis依赖,原本的MyBatis可以正常使用

(2)配置yaml文件并启动类扫描

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

启动类添加对应的mapper扫描

@SpringBootApplication
@MapperScan("com.example.demo.mapper") //mapper扫描
public class DemoApplication {
  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }
}

(3)编写实体类和对应的mapper

package com.example.demo.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("user")
public class User {
  private Long id;
  private String name;
  private Integer age;
  private String email;
}

编写mapper,同时继承BaseMapper的同时注意泛型

package com.example.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

(4)使用MP

新增

User user = new User();
user.setName("Tom");
user.setAge(20);
user.setEmail("tom@test.com");

int result = userMapper.insert(user);

System.out.println(result); // 影响行数
System.out.println(user.getId()); // 自动回填主键

根据ID查询

User user = userMapper.selectById(1);

System.out.println(user);

补充:方法引用写法;;

例:list.forEach(System.out::println);

这种写法等价于

list.forEach(x->System.out.println(x));

同理,如下写法

users.stream()
     .map(User::getName)
     .forEach(System.out::println);

等价于

users.stream()
     .map(u -> u.getName())
     .forEach(name -> System.out.println(name));

总结下来,只要看到A::B

就等同于 x -> A.B(x) 

常见注解

MP通过扫描四题类,并基于反射获取实体类信息作为数据库表信息。

之所以继承BaseMapper时要用泛型,就是为了通过反射来获取表的信息

约定类名驼峰转下划线作为表名名为id的字段作为主键;变量名驼峰转下划线作为表的字段名

常用如下注解

@TableName:指定表名,如@TableName("tb_user")。

@TableId:指定表中主键,如@TableName("id",type=IdType.AUTO),其中IdType枚举AUTO代表数据库自增,INPUT代表通过set方法程序员自己输入,ASSIGN_ID代表分配ID,默认使用雪花算法,type一定要写,若不写则默认使用雪花算法

@TableField:指定表中普通字段信息,如@TableField("username"),其中如果数据库字段is开头并且是bool值的一定要用该注解,其中数据库中字段名为类似order这样的关键字,那需要使用转义@TableField("`order`"),如果实体类中有一个字段根本就不是数据库字段,那么需要@TabelField(exist=false)来标记

常用配置

使用mp常见配置如下

server:
  port: 8080
  # Spring Boot 项目启动端口

spring:
  datasource:
    driver-class-name: org.sqlite.JDBC
    # 数据库驱动类
    # 如果你用的是 MySQL,通常写成 com.mysql.cj.jdbc.Driver

    url: jdbc:sqlite:./data/warehouse.db
    # 数据库连接地址
    # MySQL 示例:
    # url: jdbc:mysql://localhost:3306/your_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai

    username: 
    password: 
    # SQLite 一般不需要用户名和密码
    # MySQL 则需要填写对应账号密码

mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml
  # mapper.xml 文件的位置
  # 如果你使用 XML 编写 SQL,这个配置很重要
  # classpath* 表示扫描多个路径下的资源

  type-aliases-package: com.example.demo.entity
  # 实体类别名扫描包
  # 配置后,在 XML 中可以直接写类名而不用写全限定类名

  configuration:
    map-underscore-to-camel-case: true
    # 是否开启下划线转驼峰
    # 例如数据库字段 user_name -> 实体属性 userName

    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    # 控制台打印 SQL 日志
    # 学习阶段非常推荐打开,方便看执行了什么 SQL
    # 生产环境一般不建议这样直接打印

    cache-enabled: false
    # 是否开启 MyBatis 二级缓存
    # 一般项目里默认先不开,避免调试时出现缓存干扰

  global-config:
    db-config:
      id-type: auto
      # 主键生成策略
      # auto: 数据库自增
      # assign_id: MP 默认雪花算法生成 ID
      # input: 手动输入
      # 如果你是 SQLite/MySQL 自增主键,常见就是 auto

      table-underline: true
      # 表名和字段名是否使用下划线风格
      # 一般配合驼峰映射使用

      logic-delete-field: deleted
      # 全局逻辑删除字段名
      # 实体类中如果有 deleted 字段,可以统一配置逻辑删除

      logic-delete-value: 1
      # 逻辑已删除值

      logic-not-delete-value: 0
      # 逻辑未删除值

MP使用基本流程总结:
1.引入起步依赖

2.自定义Mapper基础BaseMapper

3.实体类上添加注解声明表相关信息

4.在application.yml中根据需要添加配置

Domain软件包及其子包

在项目的开发过程中,可以专门建立一个Domain软件包用来放置各种实体类,里面再建各种软件包例如dto类,entity数据库实体类等等,约定命名标准一般如下所示:

domain
├── entity / po / do     数据库实体类
├── dto                 数据传输对象
├── vo                  返回给前端的展示对象
├── query               查询条件对象
├── bo                  业务对象
├── param / req         前端请求参数对象
├── cmd / command       新增、修改、删除等操作命令对象
├── convert             对象转换类
├── enums               枚举类
└── constants           常量类

条件构造器

查询构造器

查询主要使用QueryWrapper和LambdaQueryWrapper

QueryWrapper写法更多属于硬编码因此了解即可

QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", "Tom");

LambdaQueryWrapper写法如下,避免了硬编码问题

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getName, "Tom");

查询常用的方法:

eq

wrapper.eq(User::getName, "Tom")
       .eq(User::getAge, 18);

等价于(默认AND)

WHERE name = 'Tom' AND age = 18

比较查询

wrapper.gt(User::getAge, 18);  // >
wrapper.ge(User::getAge, 18);  // >=
wrapper.lt(User::getAge, 30);  // <
wrapper.le(User::getAge, 30);  // <=

范围查询

wrapper.between(User::getAge, 18, 30);
WHERE age BETWEEN 18 AND 30

in查询

wrapper.in(User::getId, Arrays.asList(1, 2, 3));
WHERE id IN (1,2,3)

模糊查询like、likeRight、likeLeft

wrapper.like(User::getName, "Tom");
wrapper.likeRight(User::getName, "Tom");
wrapper.likeLeft(User::getName, "Tom");
WHERE name LIKE '%Tom%';
WHERE name LIKE 'Tom%';
WHERE name LIKE '%Tom';

or

基础用法

wrapper.eq(User::getName, "Tom")
       .or()
       .eq(User::getAge, 18);
name = 'Tom' OR age = 18

推荐写法

wrapper.and(w -> w.like(User::getName, "Tom")
                  .or()
                  .like(User::getName, "Jerry"));
AND (name LIKE '%Tom%' OR name LIKE '%Jerry%')

分页查询

分页查询比较复杂。

1.首先我们要专门创建一个用于分页的对象Page

Page<User> page = new Page<>(1, 10);

1就是当前页,10就是每页的条数,实际开发中要在controller层接收分页的参数,也就是单前页和每页的条数,然后在这里替换为前端传过来的值。

2.执行查询操作,并把Page对象传入,同时接收时也要使用Page来接收

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(User::getName, "Tom");

Page<User> result = userMapper.selectPage(page, wrapper);

3.接收的result处理

List<User> records = result.getRecords();   // 当前页数据
long total = result.getTotal();             // 总条数
long pages = result.getPages();             // 总页数
long current = result.getCurrent();         // 当前页
long size = result.getSize();               // 每页条数

排序orderByDesc和orderByAsc

wrapper.orderByDesc(User::getCreateTime);
wrapper.orderByAsc(User::getId);

动态排序orderBy

所谓动态排序,就是排序字段、排序方向、是否排序不写死,根据前端业务决定。比如用户可以根据需求,根据时间排序,根据年龄来排序。

因此就有了orderBy(condition,isAsc,column)这个方法,condition代表是否要加上这个排序,如果为true则加上这个排序,如果为false则不加上这个排序isAsc为true则按升序也就是ASC排序,如果为false则按降序也就是DESC排序column就是排序的字段,比如User::getId

动态排序用例:

假设前端传入两个参数sortField和sortOrder,比如sortField = createTime,sortOrder = desc,那么推荐使用下面这种写法(白名单控制)

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();

if ("age".equals(sortField)) {
    wrapper.orderBy(true, "asc".equalsIgnoreCase(sortOrder), User::getAge);
} else if ("createTime".equals(sortField)) {
    wrapper.orderBy(true, "asc".equalsIgnoreCase(sortOrder), User::getCreateTime);
} else {
    wrapper.orderByDesc(User::getId); // 默认排序
}

last

last方法就是在SQL最后拼接一段原生SQL。

例如我现在只想读取一条数据就可以使用last如下写法:

wrapper.last("limit 1");
LIMIT 1

last只适合补充Wrapper不方便表达的尾部SQL语句,要尽可能避免滥用。

修改构造器

在MybatisPlus的Update相关语句里,一般有两种思路。

第一种就是直接根据实体对象更新

User user = new User();
user.setId(1L);
user.setName("Tom");

userMapper.updateById(user);

这种适合已知主键,并且更新某一条记录

第二种就是根据条件更新

比如把符合条件的数据,某个字段统一更改为某一个值,此时就需要使用UpdateWrapper / LambdaUpdateWrapper

LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(User::getStatus, 0)
       .set(User::getStatus, 1);

Wrapper语法可以使用如下方式记:

lambdaUpdateWrapper
    .set(要修改的字段, 新值)
    .eq(更新条件字段, 条件值);

对应的SQL

UPDATE 表名
SET 字段 = 新值
WHERE 条件

例:

LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(User::getId, 1)
       .set(User::getName, "Tom");

userMapper.update(null, wrapper);
UPDATE user
SET name = 'Tom'
WHERE id = 1

update这个方法第一个参数是对应的实体类,第二个参数是wrapper对象,既然使用了第二种写法把更新的内容写在wrapper里面,那么第一个参数自然就是null了。

更新Wrapper的where方法例如eq、in、like等和查询的一模一样

set方法

LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(User::getId, 1)
       .set(User::getName, "Alice")
       .set(User::getAge, 20)
       .set(User::getStatus, 1);

userMapper.update(null, wrapper);
UPDATE user
SET name = 'Alice',
    age = 20,
    status = 1
WHERE id = 1

动态set条件与动态where条件

wrapper.eq(User::getId, 1)
       .set(name != null, User::getName, name);

代表当name!=null这个判断为true时才会执行这个set,如果判断为false那么就不会执行这个set。

动态where条件类似

wrapper.eq(status != null, User::getStatus, status)
       .set(User::getDeleted, 1);

setSql语句

LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(User::getId, 1)
       .setSql("age = age + 1");

userMapper.update(null, wrapper);
UPDATE user
SET age = age + 1
WHERE id = 1

setSql要谨防SQL注入的问题

增加语句

MP中没有增加所用的Wrapper,所以使用实体类来写就可以

User user = new User();
user.setName("Tom");
user.setAge(18);
user.setEmail("tom@qq.com");

userMapper.insert(user);
删除构造器

按主键删除

userMapper.deleteById(1L);
DELETE FROM user WHERE id = 1

批量删除

userMapper.deleteBatchIds(Arrays.asList(1L, 2L, 3L));
DELETE FROM user WHERE id IN (1, 2, 3)

按条件删除

按条件删除需要使用Wrapper来表示where语句

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getStatus, 0);

userMapper.delete(wrapper);
DELETE FROM user WHERE status = 0

使用Map进行删除

Map<String, Object> map = new HashMap<>();
map.put("name", "Tom");
map.put("age", 18);

userMapper.deleteByMap(map);
DELETE FROM user WHERE name = 'Tom' AND age = 18

逻辑删除

逻辑删除需要表里面有一个类似的字段is_delete,当is_delete为0时代表未删除,当is_delete为1时代表删除

首先在实体类中需要使用注解标记逻辑删除这个字段

@TableField("is_delete")
@TableLogic
private Integer isDelete;

在配置文件中也需要进行相关的配置

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: isDelete
      logic-delete-value: 1
      logic-not-delete-value: 0

这样在进行逻辑删除时就可以按照如下写法

userMapper.deleteById(1L);
UPDATE user SET is_delete = 1
WHERE id = 1 AND is_delete = 0

条件逻辑删除

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getStatus, 0);

userMapper.delete(wrapper);
UPDATE user SET is_delete = 1
WHERE status = 0 AND is_delete = 0

开启逻辑删除以后,在查询时,MP会自动过滤is_delete为1的结果。

Service接口

MP的service接口是对Mapper的进一步封装。MP提供了一个接口IService<T>,以及默认实现类ServiceImpl<M extends BaseMapper<T>, T>

使用流程:

1.定义service接口

public interface UserService extends IService<User> {
}

2.实现

@Service
public class UserServiceImpl 
    extends ServiceImpl<UserMapper, User> 
    implements UserService {
}

3.直接使用

@Autowired
private UserService userService;

userService.save(user);
userService.getById(1);
userService.list();

存在意义

如果使用MP的service接口,可以在写单表查询的代码上更加方便,在单表查询的时候如下所示:

@Service
public class ProductService {

    @Autowired
    private ProductMapper productMapper;

    public Product getById(Long id) {
        return productMapper.selectById(id);
    }

    public void save(Product product) {
        productMapper.insert(product);
    }

    public void update(Product product) {
        productMapper.updateById(product);
    }

    public List<Product> list() {
        return productMapper.selectList(null);
    }
}

这些代码本质就是为了帮运代码,写service就是为了写单表查询的mapper。

使用了MP的Service接口,就可以做到在Controller直接使用service来完成单表查询,而不是为了写代码而写代码。

Serivce接口消灭了上面例子这些模板代码。

baseMapper

在使用了Service接口以后,也不再需要构造Mapper了,直接使用Service接口的baseMapper就可以直接使用Mapper了。例如:在自定义xml写复杂SQL语句,直接使用baseMapper就可以,不需要创建对应的Mapper。

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Override
    public List<User> getAdultUsers(Integer age) {
        return baseMapper.selectAdultUsers(age);
    }
}
IService的lambdaQuery

以前在写复杂的查询时,首先创建QueryWrapper实例,然后写查询条件,最后使用DAO层xxxMapper.selectList(queryWrapper);来进行查询,太麻烦了。示例如下

QueryWrapper<Goods> wrapper = new QueryWrapper<>();

wrapper.like("goods_name", "螺丝")
       .eq("category_id", 1)
       .gt("stock", 10)
       .lt("price", 100);

List<Goods> list = goodsMapper.selectList(wrapper);

现在使用IService的lambdaQuery,就不再需要创建wrapper对象了,甚至不需要调用Mapper对象,直接调用xxxSerivce.lambdaQuery(),或直接在xxxService里面使用lambdaQuery()就可以完成一次复杂的查询。

写法如下:

public List<Goods> listGoods(GoodsQuery query) {
    return this.lambdaQuery()
            // 商品名称模糊查询
            .like(query.getKeyword() != null && !query.getKeyword().isBlank(),
                    Goods::getGoodsName,
                    query.getKeyword())

            // 商品分类精确查询
            .eq(query.getCategoryId() != null,
                    Goods::getCategoryId,
                    query.getCategoryId())

            // 商品状态精确查询
            .eq(query.getStatus() != null,
                    Goods::getStatus,
                    query.getStatus())

            // 库存大于最小库存
            .gt(query.getMinStock() != null,
                    Goods::getStock,
                    query.getMinStock())

            // 库存小于最大库存
            .lt(query.getMaxStock() != null,
                    Goods::getStock,
                    query.getMaxStock())

            // 价格大于等于最低价
            .ge(query.getMinPrice() != null,
                    Goods::getPrice,
                    query.getMinPrice())

            // 价格小于等于最高价
            .le(query.getMaxPrice() != null,
                    Goods::getPrice,
                    query.getMaxPrice())

            // 按创建时间倒序
            .orderByDesc(Goods::getCreateTime)

            // 执行查询
            .list();
}

结构如下:

.like(条件是否生效, 字段, 值)

其中.list()代表查询多条数据,.one()代表查询一条数据,.oneOpt()返回Optional<T>可以避免空指针,.count()查询数量,.exists()判断是否存在,.page()分页查询,.remove()根据条件删除,根据条件更新

IService的lambdaUpdate

与lambdaQuery同理,最后要加上.update()执行

lambdaUpdate()
    .eq(...)     // 条件
    .set(...)    // 要修改的字段
    .update();   // 执行
IService批量新增

假设现在有500条数据,使用for循环并且一个一个save太慢了。

所以现在使用批处理saveBatch()也就是MP的批量新增API来进行。基本写法如下所示:

List<Goods> goodsList = new ArrayList<>();

goodsList.add(new Goods("螺丝刀", 100));
goodsList.add(new Goods("扳手", 50));
goodsList.add(new Goods("电钻", 20));

boolean success = this.saveBatch(goodsList);

同时还可以指定批次大小,例如每500次提交一次:

boolean success = this.saveBatch(goodsList, 500);
循环依赖问题及DB静态工具

在使用Service接口的时候会出现循环依赖的情况,A调B然后B又调用A,然后出现死循环。如下所示:

ASerivce里面需要创建BService

@Service
public class AService {

    @Autowired
    private BService bService;
}

BService里面需要创建AService

@Service
public class BService {

    @Autowired
    private AService aService;
}

然后出现A->B->A->B->A....然后就死循环。这在设计上是不允许的,所以在设计过程中也应该尽可能避免,而不是想办法解决

对于一些简单的循环依赖,一些简单的查询,可以考虑使用Db静态工具

Db静态工具可以做到不注入Mapper,不注入Service就直接增删改查。语法几乎和Service接口一样。如下所示:

Goods goods = Db.getById(1L, Goods.class);

List<Goods> list = Db.lambdaQuery(Goods.class)
        .eq(Goods::getStatus, 1)
        .list();

Goods goods = Db.lambdaQuery(Goods.class)
        .eq(Goods::getGoodsName, "螺丝刀")
        .one();
操作方法
根据 ID 查Db.getById(id, Entity.class)
条件查多条Db.lambdaQuery(Entity.class).list()
条件查一条Db.lambdaQuery(Entity.class).one()
查数量Db.lambdaQuery(Entity.class).count()
是否存在Db.lambdaQuery(Entity.class).exists()
新增一条Db.save(entity)
批量新增Db.saveBatch(list)
根据 ID 修改Db.updateById(entity)
条件修改Db.lambdaUpdate(Entity.class).set(...).update()
批量修改Db.updateBatchById(list)
根据 ID 删除Db.removeById(id, Entity.class)
批量删除Db.removeByIds(ids, Entity.class)
条件删除Db.lambdaUpdate(Entity.class).eq(...).remove()
枚举以及枚举处理器MybatisEnumTypeHandler

对于一些status状态使用枚举是最好的选择,因为直观

public enum GoodsStatusEnum {
    ENABLED,
    DISABLED,
    DELETED
}

赋值+判断

GoodsStatusEnum status = GoodsStatusEnum.ENABLED;

if (status == GoodsStatusEnum.ENABLED) {
    System.out.println("商品已启用");
}

实际使用时,枚举会加上一些数据值和中文描述

@Getter
@AllArgsConstructor
public enum GoodsStatusEnum {

    ENABLED(1, "启用"),
    DISABLED(0, "禁用"),
    DELETED(-1, "删除");

    private final Integer code;
    private final String desc;
}
GoodsStatusEnum status = GoodsStatusEnum.ENABLED;

System.out.println(status.getCode()); // 1
System.out.println(status.getDesc()); // 启用

通过code来找到枚举的方法

@Getter
@AllArgsConstructor
public enum GoodsStatusEnum {

    ENABLED(1, "启用"),
    DISABLED(0, "禁用"),
    DELETED(-1, "删除");

    private final Integer code;
    private final String desc;

    public static GoodsStatusEnum of(Integer code) {
        if (code == null) {
            return null;
        }

        for (GoodsStatusEnum item : values()) {
            if (item.getCode().equals(code)) {
                return item;
            }
        }

        throw new IllegalArgumentException("未知商品状态:" + code);
    }
}

使用例:

GoodsStatusEnum status = GoodsStatusEnum.of(1);
System.out.println(status.getDesc()); // 启用

其中values()是枚举自带的方法,表示获取当前枚举类的所有枚举项。

在实体类中可以使用如下方法来使用枚举,同时就要使用到枚举处理器了

@Data
@TableName("goods")
public class Goods {

    private Long id;

    private String goodsName;

    private GoodsStatusEnum status;
    // 原本写法:private Integer status;
}

但是不能把枚举直接存数据库,我们只存枚举的code值,所以就要使用@EnumValue这个注解

@Getter
@AllArgsConstructor
public enum GoodsStatusEnum {

    ENABLED(1, "启用"),
    DISABLED(0, "禁用"),
    DELETED(-1, "删除");

    @EnumValue
    private final Integer code;
    // 这个枚举存入数据库时,使用code值存入数据库

    private final String desc;
}

所以GoodsStatusEnum.ENABLED存入数据库时健身1,而1从数据库中取出时会转回GoodsStatusEnum.ENABLED

枚举扩展

枚举有几个自带的方法

values() 返回所以枚举值,例如:、

GoodsStatusEnum[] arr = GoodsStatusEnum.values();

valueOf() 根据名字查找枚举,例如:

GoodsStatusEnum status = GoodsStatusEnum.valueOf("ENABLED");

name()  返回枚举名,例如:

System.out.println(GoodsStatusEnum.ENABLED.name()); // ENABLED

ordinal()  返回枚举序号,例如:

GoodsStatusEnum.ENABLED.ordinal(); // 0

一个完整的MyBatis下的枚举使用例:

@Getter
@AllArgsConstructor
public enum GoodsStatusEnum {

    ENABLED(1, "启用"),
    DISABLED(0, "禁用"),
    DELETED(-1, "删除");

    @EnumValue
    private final Integer code;

    private final String desc;

    public static GoodsStatusEnum of(Integer code) {
        if (code == null) {
            return null;
        }
        for (GoodsStatusEnum item : values()) {
            if (item.getCode().equals(code)) {
                return item;
            }
        }
        throw new IllegalArgumentException("未知状态:" + code);
    }
}
JSON处理器

没有JSON处理器时遇到JSON字段只能自己手动转,很麻烦。

有了JSON以后可以使用对象方式来存储JSON字段

@TableName(value = "goods", autoResultMap = true)
public class Goods {

    private Long id;

    private String goodsName;

    @TableField(typeHandler = JacksonTypeHandler.class)
    private ExtraInfo extraInfo;
}

在对象中存储JSON字段

@Data
public class ExtraInfo {
    private String color;
    private String size;
}

数据库中只需要一个字段来存储如下内容

{"color":"red","size":"L"}

然而拿确实如下方式

goods.getExtraInfo().getColor();
goods.getExtraInfo().getSize();

常见的JSON处理器有JacksonTypeHandler、FastjsonTypeHandler、GsonTypeHandler

建议优先使用JacksonTypeHandler

对于List这种情况,可以使用如下方式:

例如这个JSON字段:["工具","五金","常用"]

使用如下方式存

@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> tags;

完整如下

@Data
@TableName(value = "goods", autoResultMap = true)
public class Goods {

    private Long id;

    private String goodsName;

    @TableField(typeHandler = JacksonTypeHandler.class)
    private List<String> tags;
}

对象List方法

对于这种List:

[
  {"name":"颜色","value":"红色"},
  {"name":"尺寸","value":"大号"}
]

可使用如下Java类存储

@Data
public class SpecItem {
    private String name;
    private String value;
}

然后实体类这样写

@TableField(typeHandler = JacksonTypeHandler.class)
private List<SpecItem> specs;

保存方式如下

ExtraInfo extraInfo = new ExtraInfo();
extraInfo.setColor("red");
extraInfo.setSize("L");

Goods goods = new Goods();
goods.setGoodsName("螺丝刀");
goods.setExtraInfo(extraInfo);

goodsService.save(goods);

查询类似

Goods goods = goodsService.getById(1L);

ExtraInfo extraInfo = goods.getExtraInfo();

System.out.println(extraInfo.getColor());

不再需要手动JSON.parseObject()

Docker

Docker原理

一个快速构建、运行、管理的工具。

我们使用Docker安装应用时,Docker会自动搜索并下载镜像(image)。镜像不仅包含应用本身,还包含应用所需要的环境、配置、系统函数库。Docker会在运行镜像的时候创建一个隔离环境,称为容器(container)。

镜像仓库:存储和管理镜像的平台,Docker官方维护了一个公共仓库:Docker Hub。

总结:

1.Docker简单来说就是使用容器化技术,给应用程序封装独立的运行环境,每个运行环境就是一个容器,运行容器的计算机叫宿主机。Docker容器与虚拟机的最大区别是,Docker容器之间公用同一个系统内核,而每一个虚拟机都包含一个操作系统的完整内核,所以Docker比虚拟机更轻量、更小、启动速度更快。

2.镜像是容器的模板,可以把镜像类比成软件的安装包,而容器就是安装出来的软件Docker仓库就是用于存放分享镜像的地方。每个人都可以把自己的镜像上传到镜像仓库里面,其他人就可以下载并使用,Docker的官方仓库就是Docker Hub,上面存储了许多人分享的Docker镜像。

Docker下载安装MySQL实例

1.首先下载MySQL镜像,这一步相当于下载MySQL

docker pull mysql:8.4

2.启动一个MySQL容器,这里会基于刚才下载的mysql:8.4镜像启动 MySQL。

docker run -d \
  --name mysql-demo \
  -e MYSQL_ROOT_PASSWORD=123456 \
  -p 3306:3306 \
  -v mysql_data:/var/lib/mysql \
  mysql:8.4

-p 宿主机端口:容器端口

宿主机端口为外部访问时所用的端口。-p 外面访问的端口:容器里面服务真正监听的端口

以下为各参数含义

docker run 创建并运行一个容器 -d是指容器在后台运行                                                                    --name mysql-demo              容器名字,必须唯一
-e MYSQL_ROOT_PASSWORD=123456  设置 root 密码
-p 3306:3306                   把 MySQL 端口映射到你电脑的 3306
-v mysql_data:/var/lib/mysql   保存数据库数据
-d                             后台运行                                                                                                             mysql:8.4              镜像名:镜像版本

3.查看是否成功启动

输入

docker ps

若看到日志输出ready for connections则代表可以使用了

4.连接

进入容器方式连接:

输入

docker exec -it mysql-demo mysql -uroot -p

然后输入刚刚设置的密码:123456

Docker常见命令

docker pull命令

docker pull命令完整是docker pull [镜像地址]

完整内容1:docker pull [registry地址/][命名空间/]镜像名[:标签]

完整内容2:docker pull [registry地址/][命名空间/]镜像名@digest

拉取官方镜像时,可省略的内容比较多,以拉取MySQL为例,直接执行如下代码即可:

docker pull mysql

拉取官方镜像时实际省略了如下内容

docker pull docker.io/library/mysql:latest
docker.io     默认镜像仓库,也就是 Docker Hub
library       Docker Hub 官方镜像默认命名空间
mysql         镜像名
latest        默认标签

latest用于指定版本,例如我想指定8.4版本,则docker pull mysql:8.4即可。

官方镜像的特点就是:通常不需要写用户名或组织名

拉取非官方镜像时,需要完整写出全部内容,例如:

docker pull bitnami/mysql:8.4
bitnami       命名空间,表示这个镜像由 bitnami 维护
mysql         镜像名
8.4           标签

docker pull 用户名或组织名/镜像名:标签

区别:

docker pull mysql:8.4 表示Docker Hub官方MySQL镜像

docker pull bitnami/mysql:8.4 表示Bitnami维护的MySQL镜像。

若镜像不在Docker Hub而在其他的registry(地址),就要把 registry 地址也写出来。例如,GitHub Container Registry拉取

docker pull ghcr.io/example-org/example-app:1.0
ghcr.io        registry 地址
example-org    命名空间/组织名
example-app    镜像名
1.0            标签

再例如公司的私有仓库,就是

docker pull registry.example.com/backend/user-service:2.3.1
registry.example.com   私有镜像仓库地址
backend                命名空间或项目组
user-service           镜像名
2.3.1                  标签

以下是简单记忆方式:

没有 /
通常是 Docker Hub 官方镜像,例如 mysql、redis、nginx

有一个 /
通常是 Docker Hub 上某个用户或组织的镜像,例如 bitnami/mysql

前面带域名
通常是其他镜像仓库,例如 ghcr.io/org/app

docker run与docker ps命令

docker run mysql 就可以启动一个容器  

docker run属性

-d :detached mode表示分离模式,这样容器就可以在后台执行,不会阻塞当前的窗口。若不使用,则会一直在窗口中打印日志。使用例:docker run -d mysql。

-p:容器的网络与宿主机的网络是隔离的,因此就需要一个启动参数-p,把容器内的端口与宿主机的端口做一个映射,-p 宿主机端口:容器内的端口 这样在访问宿主机的端口的时候就等于访问到了容器内的端口。使用例:docker run -p 80:80 nginx。在容器外使用localhost:80,这样就打开了容器内部署的nginx。

-v:与-p类似,-v则是把宿主机与容器的文件目录做一个绑定,容器内对绑定的文件夹进行修改,宿主机的文件夹也会同时被修改,反过来同理。该操作也叫挂载卷。这样可在删除容器时,被绑定的容器就可以被保存,相当于对文件做了一个持久化的操作-v 宿主机目录:容器目录。直接在docker run时就使用-v绑定操作,叫做绑定挂载。让Docker自动创建一个存储空间,为这个存储空间起一个名字,使用-v挂载时直接使用名字,这种方式叫做命名卷挂载

--network:指定加入的子网,例如:--network network1。加入子网1。使用--network host则代表使用host模式,共享宿主机网络。

补充:命名卷挂载操作方法

使用sudo docker volume create 命名卷的名字    指令创建命名卷,如下所示:

sudo docker volume create nginx_html

然后直接使用命名卷nginx_html创建容器:

sudo docker run -d -p80:80 -v nginx_html:/usr/share/nginx/html nginx。这样就是使用命名卷挂载的方式。

使用sudo docker volume inspect 挂载卷名字 来查看挂载卷的目录,如下所示:

sudo docker volume inspect nginx_html

回车后大致内容如下

Mountpoint的位置就是路径。

docker volume list可以查看所有创建过的卷。

docekr volume rm nginx_html可以删除一个卷。

docker volume prune -a删除所有没有任何容器在使用的卷。

-e :用来在docker run时往容器里面传递环境变量。例如在MySQL数据库中,就需要输入密码

-e MYSQL_ROOT_PASSWORD=123456  设置 root 密码。

--name :给一个容器起一个名字,该名字必须唯一且不可重复。

-it : 可以让我的控制台进入容器进行交互。

--rm :指容器停止时就把容器删除掉,用于临时调试某个容器。

--restart :用来配置容器在停止时的重启策略。常用:--restart always,只要容器停止了,就会立即重启,包含容器因为内部错误崩溃,或者宿主机断电等等场景。--restart unless-stopped,与always类似,有一个区别在于,手动停止的容器就不会尝试重启了。

输入docker ps 就可以查看正在运行的容器ps(process status)

-a:docker ps命令-a 代表all,可以看到所有容器,包括正在运行的和已经停止的容器

在ps输出的内容中CONTAINER ID:容器唯一的ID;IMAGE:镜像的模具;NAMES:镜像的名字。

docker start与docker stop命令

docker start 容器ID/名字 使用该命令后面接容器的ID或者名字可以把某个容器再次启动起来。

docker stop 容器ID/名字 使用该命令后面接容器的ID或者名字可以停止某个容器的运行。

docker inspect命令

docker inspect 容器ID,就可以看到docker的一些详细配置信息

docker create命令

docker create和docker run命令非常相似,唯一的区别是docker create是创建了以后不启动,docker run是创建了以后启动。docker create运行了以后还需要配合docker start使用

docker logs命令

docker logs 容器ID/或名字,后面接容器的ID或者名字,可以查看容器的日志。

-f :滚动查看日期,这样日志就可以及时刷新。

docker exec命令

docker exec xxxxx(容器ID) linux命令   可以再容器的内部执行Linux命令。 

-it:使用docker exec -it abcd1234 /bin/sh 可以进入到一个正在运行的Docker容器内部,获得一个交互式的命令行环境。

补充:在容器内的Linux为保证使用效率,会做一些阉割,因此有时可能会要额外下载一些工具。
1.使用docker exec -it abcd1234 /bin/sh进入到一个容器中。
2.使用cat /etc/os-release 查看容器的发行版。
3.若为Debian,使用apt update更新一下索引。
4.使用apt install vim安装

命令的别名

命令别名就是:给一段常用命令起一个短名字,之后输入短名字就等于输入完整命令。

例如我经常写如下命令

docker ps

就可以设置如下别名

alias dps='docker ps'

以后直接输入dps就等于输入了docker ps这个命令

数据卷

数据卷是一个虚拟目录,是容器内目录与宿主机目录映射的桥梁。相当于做了一个桥梁

在执行docker run命令时,使用 -v 数据卷:容器内目录 可完成数据卷挂载。在创建容器时,若挂载了数据卷和数据卷不存在则会自动创建新数据卷。

Docker网络

内部使用的是桥接模式

docker network network1:创建一个子网。

通过子网就可以让容器在内部互相访问。同时使用了子网以后可以不需要输入IP地址,直接通过子网名就可以访问。 

docker network list:打印所有的网络。

docker network rm xxxxx:删除指定ID的网络。

Docker Compose

Docker Compose 是用来一次性定义和管理多个容器的工具。

例如:现在有一个项目,需要Java后端、MySQL、Redis、Nginx网关等。一个一个docker run太慢了,现在直接写一个Docker Compose的yaml文件直接一次性启动。

Docker Compose 的作用就是:把这些容器怎么启动,统一写进一个配置文件里,然后用一条命令启动整套环境。

使用例:现在要启动如上内容,直接执行如下YAML文件即可

services:
  mysql:
    image: mysql:8.4
    container_name: mysql-demo
    environment:
      MYSQL_ROOT_PASSWORD: 123456
      MYSQL_DATABASE: testdb
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql

  redis:
    image: redis:7
    container_name: redis-demo
    ports:
      - "6379:6379"

volumes:
  mysql_data:

然后再输入如下命令执行YML文件

docker compose up -d

项目就启动了。

总结

1.Docker Compose把多个容器定义称为多个service,如下的例子中就定义了mysql、redis、app三个service。

services:
  mysql:
    image: mysql:8.4

  redis:
    image: redis:7

  app:
    image: my-app:1.0

2.把复杂的 docker run 参数写成配置文件。

以前写docker run时要写一大堆-d -e -p等等。现在直接使用Compose就可以使用yaml直观的看到各种配置参数。

3.自动创建容器网络

如下例子中,Compose 会自动创建一个网络,让这些服务可以互相访问。而且服务之间可以直接用服务名当域名。比如后端连接 MySQL 时,地址不是:127.0.0.1,而是mysql,也就是说服务名 = 容器之间访问用的主机名,例如SpringBoot的配置就可能是spring.datasource.url=jdbc:mysql://mysql:3306/testdb。

services:
  mysql:
    image: mysql:8.4

  app:
    image: my-app:1.0

其中:宿主机访问容器:用 localhost + 映射端口容器访问容器:用 service 名 + 容器内部端口

例如:

services:
  mysql:
    ports:
      - "3307:3306"

  app:
    image: my-app:1.0

在这个例子中,宿主机分为MySQL就是localhost:3307,但是Docker容器访问MySQL就是mysql:3306

Dockerfile

镜像结构:入口、层、基础镜像。

入口(Entrypoint):镜像运行入口,一般是程序启动的脚本和参数。

层(Layer):添加安装包、依赖、配置等,每次操作都形成新的一层。

基础镜像(BaseImage):应用依赖的系统函数库、环境、配置、文件等 

Dockerfile就是一个文本文件,其中包含一个个指令(Instruction),用指令来说明要执行什么操作来构建镜像,将来Docker可以根据Dockerfile帮我们构建镜像。常见指令如下:

Docker 会逐行读取 Dockerfile,然后把它构建成一个 Docker 镜像 Image。官方定义也是:Dockerfile 是包含镜像构建指令的文本文件。

微服务

单体架构:把所有的业务功能集中在一个项目中开发,打成一个包部署。

微服务架构:是服务化思想指导下的一套最佳实践架构方案。服务化,就是把单体架构中的功能模块拆分成多个独立项目。粒度小、团队自治、服务自治。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐