SpringCloud Alibaba微服务学习20260107
微服务简介
微服务是一种结构风格,代表着一种通过将应用程序拆分成小型、独立的功能模块(服务)的开发方式。
每个模块实现独立的业务功能不限语言,不限技术,每个服务之间通过轻量级通信机制(例如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 是包含镜像构建指令的文本文件。
微服务
单体架构:把所有的业务功能集中在一个项目中开发,打成一个包部署。
微服务架构:是服务化思想指导下的一套最佳实践架构方案。服务化,就是把单体架构中的功能模块拆分成多个独立项目。粒度小、团队自治、服务自治。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)