# house_Lease **Repository Path**: LA-admin/house_-lease ## Basic Information - **Project Name**: house_Lease - **Description**: 尚硅谷尚庭公寓项目学习笔记由开发技术实现-->项目部署(docker) - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-07-16 - **Last Updated**: 2024-08-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 尚硅谷--尚庭公寓 ## 技术选型: ### 后端技术: | 技术 | 说明 | 备注 | | :----------: | :----------------------------------------------------------: | -------------------------------------- | | HikariCP | 数据库连接池(在Spring Boot 2.0及以后版本中,HikariCP是默认的连接池实现,也可以选用alibaba的Druid) | 在Mybatis-Plus依赖中带有HikariCP的依赖 | | SpringBoot | Web应用开发框架 version:3.0.4 | | | Mybatis-Plus | | | | | | | ## 技术实现: ### 1.项目实体类中公共字段抽出 (例如`id`、`create_time`、`update_time`、`is_deleted`)抽取到一个基类,进行统一管理,然后让各实体类继承该基类。 ```java @Data public class BaseEntity implements Serializable { @Schema(description = "主键") //knife4j注解,用于在接口文档中标识属性描述 @TableId(value = "id", type = IdType.AUTO) //mybatisPlus注解,用于标识对应数据库的字段名称 private Long id; @Schema(description = "创建时间") @TableField(value = "create_time") private Date createTime; @Schema(description = "更新时间") @TableField(value = "update_time") private Date updateTime; @Schema(description = "逻辑删除") @TableField("is_deleted") private Byte isDeleted; } ``` ### 2.实体类注解@Data @Builder 结合使用 @Data 和 @Builder 注解,你可以创建一个既有完整的访问器方法,也有构建器模式的实体类 ```java @Schema(description = "公寓&配套关系") //knife4j注解,用于在接口文档中标识属性描述 @TableName(value = "apartment_facility") //mybatisPlus注解,用于标识对应数据库表的名称 @Data //lombok注解,组合注解,等价于同时使用了@ToString、@EqualsAndHashCode、@Getter、@Setter、@RequiredArgsConstructor @Builder //lombok注解,为类生成一个构建器(Builder)模式,允许你以一种流畅且类型安全的方式来构造对象 public class ApartmentFacility extends BaseEntity { private static final long serialVersionUID = 1L; @Schema(description = "公寓id") @TableField(value = "apartment_id") private Long apartmentId; @Schema(description = "设施id") @TableField(value = "facility_id") private Long facilityId; } ``` ### 3.实体类中的状态字段(例如`status`)或类型字段(例如`type`),全部使用枚举类型 状态(类型)字段,在数据库中通常用一个数字表示一个状态(类型)。 例如:订单状态(1:待支付,2:待发货,3:待收货,4:已收货,5:已完结)。若实体类中对应的字段也用数字类型,例如`int`,那么程序中就会有大量的如下代码: ```java order.setStatus(1); if (order.getStatus() == 1) { order.setStatus(2); } ``` 这些代码后期维护起来会十分麻烦,所以本项目中所有的此类字段均使用枚举类型。 例如上述订单状态可定义为以下枚举: - 订单实体类中的状态字段定义为`Status`类型: ```java public enum Status { CANCEL(0, "已取消"), WAIT_PAY(1, "待支付"), WAIT_TRANSFER(2, "待发货"), WAIT_RECEIPT(3, "待收货"), RECEIVE(4, "已收货"), COMPLETE(5, "已完结"); private final Integer value; private final String desc; public Integer value() { return value; } public String desc() { return desc; } } ``` ```java @Data public class Order{ private Integer id; private Integer userId; private Status status; ... } ``` ```java order.setStatus(Status.WAIT_PAY); ``` - 这样上述代码便可调整为如下效果,后期维护起来会容易许多。 **实际项目中的使用:** - 创建一个枚举基类方法接口(所有的枚举类都实现该接口中的方法) ```java public interface BaseEnum { Integer getCode(); String getName(); } ``` - 枚举类 ```java public enum BaseStatus implements BaseEnum { ENABLE(1, "正常"), DISABLE(0, "禁用"); @EnumValue //指定存放数据库的属性 @JsonValue //指定返回前端界面的属性 private Integer code; private String name; BaseStatus(Integer code, String name) { this.code = code; this.name = name; } @Override public Integer getCode() { return this.code; } @Override public String getName() { return this.name; } } ``` ### 4.实体类序列化实现Serializable接口 即序列化接口 在项目中我们使用了redis进行对象缓存,使用redis缓存我们必须要对对象进行序列化,这个接口可以方便我们对实体对象进行缓存 ### 5.Mybatis-Plus逻辑删除功能 在实际的项目当中,数据库中的数据表可能会采用逻辑删除策略,所以我们在进行数据时均需要增加过滤条件`is_deleted=0`,上述操作虽不难实现,但是每个查询接口都要考虑到,也显得有些繁琐。为简化上述操作,可以使用Mybatis-Plus提供的逻辑删除功能,它可以自动为查询操作增加`is_deleted=0`过滤条件,并将删除操作转为更新语句。具体配置如下:[官方文档](https://baomidou.com/guides/logic-delete/) - **步骤一:** 在`application.yml`中增加如下内容 ```yaml mybatis-plus: global-config: db-config: logic-delete-field: is_deleted # 全局逻辑删除的实体字段名--需要填的是实体类中的属性名,不是数据表中的(配置后可以忽略不配置步骤二) #此处如果与mybatis-plus默认一样,则下述两句可以不用配置 logic-delete-value: 1 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) ``` - **步骤二:** 在实体类中的删除标识字段上增加`@TableLogic`注解 *用于标识逻辑删除字段,与步骤一的`logic-delete-filed: is_deleted`作用一样* ```java @Data public class BaseEntity { @Schema(description = "主键") @TableId(value = "id", type = IdType.AUTO) private Long id; @Schema(description = "创建时间") @JsonIgnore private Date createTime; @Schema(description = "更新时间") @JsonIgnore private Date updateTime; @Schema(description = "逻辑删除") @JsonIgnore @TableLogic @TableField("is_deleted") private Byte isDeleted; } ``` **注意**:逻辑删除功能只对Mybatis-Plus自动注入的sql起效,也就是说,对于手动在`Mapper.xml`文件配置的sql不会生效,需要单独考虑。 ### 6. Jackson 库中`@JsonIgnore`注解--用于忽略特定字段 - **忽略特定字段**通常情况下接口响应的Json对象中并不需要`create_time`、`update_time`、`is_deleted`等字段,这时只需在实体类中的相应字段添加`@JsonIgnore`注解,该字段就会在序列化时被忽略。具体配置如下,详细信息可参考Jackson[官方文档]([Releases · FasterXML/jackson-annotations (github.com)](https://github.com/FasterXML/jackson-annotations?tab=readme-ov-file#annotations-for-ignoring-properties))。 > `@JsonIgnore` 是 Jackson 库中的一个注解,用于在 JSON 序列化和反序列化过程中忽略 Java 对象中的特定属性。当你在一个类的属性或 getter 方法上添加 `@JsonIgnore` 注解时,Jackson 在将对象转换为 JSON 字符串时会忽略这个属性,同样地,在将 JSON 字符串反序列化为 Java 对象时,也会忽略对应的 JSON 字段。 ```java @Data public class BaseEntity { @Schema(description = "主键") @TableId(value = "id", type = IdType.AUTO) private Long id; @Schema(description = "创建时间") @JsonIgnore @TableField(value = "create_time") private Date createTime; @Schema(description = "更新时间") @JsonIgnore @TableField(value = "update_time") private Date updateTime; @Schema(description = "逻辑删除") @JsonIgnore @TableField("is_deleted") private Byte isDeleted; } ``` **我们在controller类上会添加`@RestController`将我们的返回结果序列化成Json返回给前端,这个默认的序列化器就是Jackson** ### 7.Mybatis-Plus自动填充字段 保存或更新数据时,前端通常不会传入`isDeleted`、`createTime`、`updateTime`这三个字段,因此我们需要手动赋值。但是数据库中几乎每张表都有上述字段,所以手动去赋值就显得有些繁琐。为简化上述操作,我们可采取以下措施。 - `is_deleted`字段:可将数据库中该字段的默认值设置为0。 - `create_time`和`update_time`:可使用mybatis-plus的自动填充功能,所谓自动填充,就是通过统一配置,在插入或更新数据时,自动为某些字段赋值,具体配置如下,详细信息可参考[官方文档](https://baomidou.com/guides/auto-fill-field/)。 > **原理:**自动填充功能通过实现 `com.baomidou.mybatisplus.core.handlers.MetaObjectHandler` 接口来实现。你需要创建一个类来实现这个接口,并在其中定义插入和更新时的填充逻辑。 1. 在实体类中用`@TableField` 注解来标记哪些字段需要自动填充,并指定填充的策略(即配置触发填充的时机)。 ```java @Data public class BaseEntity { @Schema(description = "主键") @TableId(value = "id", type = IdType.AUTO) private Long id; @Schema(description = "创建时间") @JsonIgnore @TableField(value = "create_time", fill = FieldFill.INSERT) private Date createTime; @Schema(description = "更新时间") @JsonIgnore @TableField(value = "update_time", fill = FieldFill.UPDATE) private Date updateTime; @Schema(description = "逻辑删除") @JsonIgnore @TableLogic @TableField("is_deleted") private Byte isDeleted; } ``` 2. 实现 MetaObjectHandler 创建一个类来实现 `MetaObjectHandler` 接口,并重写 `insertFill` 和 `updateFill` 方法。 ```java @Slf4j @Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { log.info("开始插入填充..."); this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { log.info("开始更新填充..."); this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } } ``` ### 8.枚举类型前后端交互流程 image-20240716210333895 ​ 前后端交互所传递的数据中**type**字段为**数字**(1/2) **具体转换流程如图所示:** - **请求流程** image-20240716210333895 > ​ **说明:** > > - SpringMVC中的`WebDataBinder`组件负责将HTTP的请求参数绑定到Controller方法的参数,并实现参数类型的转换。 > - Mybatis中的`TypeHandler`用于处理Java中的实体对象与数据库之间的数据类型转换。 - **响应流程** 枚举类型转换过程-响应体.drawio > **说明:** > > - SpringMVC中的`HTTPMessageConverter`组件负责将带有@RequestBody注解的Controller方法的返回值(Java对象)转换为HTTP响应体中的JSON字符串,或者将请求体中的JSON字符串转换为Controller方法中的参数(Java对象) > > **例如:** > > ![枚举类型转换过程-请求体.drawio](images/README.assets/枚举类型转换过程-请求体.drawio.png) **类型转换原理:** **注:**HTTP请求不区分参数的数据类型(如整数、浮点数、字符串、布尔值等),因为HTTP协议是**基于文本传输**(string)的协议,它主要通过请求行(Request Line)、请求头(Headers)和请求体(Body)来传输信息。 #### 1.**WebDataBinder枚举类型转换** > - `WebDataBinder`依赖于`Converter`实现类型转换,若Controller方法声明的`@RequestParam`参数的类型不是`String`,`WebDataBinder`就会自动进行数据类型转换。SpringMVC提供了常用类型的转换器,例如`String`到`Integer`、`String`到`Date`,`String`到`Boolean`等等,其中也包括`String`到枚举类型,但是`String`到枚举类型的**默认转换规则**是根据实例名称("APARTMENT")转换为枚举对象实例(ItemType.APARTMENT)。 > > - 关于WebDataBinder枚举类型默认转换规则: > > > ```java > public enum ItemType implements BaseEnum { > > APARTMENT(1, "公寓"), > ROOM(2, "房间"); > //略 > } > ``` > > 上述代码中,我们的实例名称为`APARTMENT`和`ROOM`,因此默认的枚举类型转换规则会将这两个实例转换为`ItemType.APARTMENT`和`ItemType.ROOM`两个枚举实例对象。无法直接通过我们的Code转换为枚举对象实例,因此我们需要自定义转换器实现类型转换。 > > - 若想实现`code`属性到枚举对象实例的转换,需要自定义`Converter`,代码如下,具体内容可参考[官方文档](https://docs.spring.io/spring-framework/reference/core/validation/convert.html#core-convert-Converter-API)。 > > 1. 编写自定义转换器Converter (实现Spring官方提供的Converter接口) > > ```java > @Component > public class StringToItemTypeConverter implements Converter { > @Override > public ItemType convert(String code) { > //我们根据source的值去判断我们应该返回哪一个ItemType枚举实例对象 > //ItemType[] values = ItemType.class.getEnumConstants(); //这个方法也可以获取到枚举的实例对象数组 > ItemType[] values = ItemType.values(); > for(ItemType itemType:values){ > if(itemType.getCode().equals(Integer.valueOf(code))){ > return itemType; > } > } > throw new IllegalArgumentException("code:"+code+"非法"); //这个异常一般用于方法参数验证 > } > } > ``` > > 2. 在SpringMVC中注册自定义转换器 > > 我们在使用自己注册的converter时需要先向springMvc注册一下自己的converter,只有springMVC感知到了我们的这个converter后才可以用它去做类型转换 > > 我们想要向SpringMVC注册converter之前需要先创建一个自己的配置类 (WebMvcConfiguration),并且这个配置类要实现webmvcconfigurer这个接口,这个接口是由SpringMVC专门提供的一个用于自定义配置的接口,只要我们实现了这个接口,就可以去完成各项与SpringMVC相关的配置(比如注册自定义converter以及注册为自定义拦截器等等等) > > 简洁代码版: > > ```java > > @Configuration > > public class WebMvcConfiguration implements WebMvcConfigurer { > > @Autowired > private StringToItemTypeConverter stringToItemTypeConverter; > > @Override > public void addFormatters(FormatterRegistry registry) { > registry.addConverter(stringToItemTypeConverter); > } > } > ``` > > ```java > /** > * 向SpringMVC注册自定义配置类 > */ > @Configuration > /** > * WebMvcConfigurer这个接口是由springmvc专门提供的一个用于自定义配置的接口, > * 只要我们实现了这个接口,就可以去完成各项与springmvc相关的配置 > * (比如注册自定义converter以及注册为自定义拦截器等等等) > */ > public class WebMvcConfiguration implements WebMvcConfigurer { > > //第一种:利用SpringBoot自动注入(需要在编写自定义转换器类上加@Component注解将他注入Spring容器,这样我们才能在此处利用@Autowired自动注入) > @Autowired > private StringToItemTypeConverter stringToItemTypeConverter; > > /** > * addFormatters方法的主要作用是注册格式化器(Formatter),用于处理数据绑定和格式转换。 > * 在Spring MVC框架中,格式化器可以帮助我们将请求参数或模型对象转换为适当的类型,并将响应 > * 结果转换为适合客户端接收的数据格式。 > * @param registry > */ > @Override > public void addFormatters(FormatterRegistry registry) { > registry.addConverter(stringToItemTypeConverter); > //第二种:可以直接选择new StringToItemTypeConverter > //registry.addConverter(new StringToItemTypeConverter()); > } > } > ``` > > 但是我们有很多的枚举类型都需要考虑类型转换这个问题,按照上述思路,我们需要为每个枚举类型都定义一个Converter,并且每个Converter的转换逻辑都完全相同,针对这种情况,我们使用**`ConverterFactory`**接口更为合适,这个接口可以将同一个转换逻辑应用到一个接口的所有实现类,因此我们可以定义一个`BaseEnum`接口,然后另所有的枚举类都实现该接口,然后就可以自定义`ConverterFactory`,集中编写各枚举类的转换逻辑了。具体实现如下:[官方文档](https://docs.spring.io/spring-framework/reference/core/validation/convert.html) > > 1. 定义一个所有枚举类的父类接口`BaseEnum`,所有的枚举类都实现这个接口 > > ```java > public interface BaseEnum { > Integer getCode(); > String getName(); > } > ``` > > 2. 编写自定义转换器ConverterFactory (实现Spring官方提供的ConverterFactory 接口) > > ```java > /** > * ConverterFactory枚举类型转换器 > * 由当前这个转换器可以实现由类型到枚举父类的所有子类的类型转换 > */ > @Component > public class StringToBaseEnumConverterFactory implements ConverterFactory { > /** > * 分析以下这个getConverter方法 > * 实际是实现由目标类型得到从类型到目标类型的Converter对象 > * @param targetType > * @return > * @param > */ > @Override > public Converter getConverter(Class targetType) { > //我们这里可以直接new Converter对象 > return new Converter() { > @Override > public T convert(String code) { > //当SpringMVC框架需要某个枚举类型的Converter的时候,会自己调用getConverter并传入目标类型的class对象targetType > //我们可以通过枚举类型的class对象获取到他的全部实例 > T[] enumConstants = targetType.getEnumConstants(); > for (T enumConstant : enumConstants) { > if(enumConstant.getCode().equals(Integer.valueOf(code))){ > return enumConstant; > } > } > throw new IllegalArgumentException("code:"+code+"非法"); //这个异常一般用于方法参数验证 > } > }; > } > } > ``` > > 3. 在SpringMVC中注册自定义转换器 > > ```java > @Configuration > /** > * WebMvcConfigurer这个接口是由springmvc专门提供的一个用于自定义配置的接口, > * 只要我们实现了这个接口,就可以去完成各项与springmvc相关的配置 > * (比如注册自定义converter以及注册为自定义拦截器等等等) > */ > public class WebMvcConfiguration implements WebMvcConfigurer { > > @Autowired > private StringToBaseEnumConverterFactory stringToBaseEnumConverterFactory; > > /** > * addFormatters方法的主要作用是注册格式化器(Formatter),用于处理数据绑定和格式转换。 > * 在Spring MVC框架中,格式化器可以帮助我们将请求参数或模型对象转换为适当的类型,并将响应 > * 结果转换为适合客户端接收的数据格式。 > * @param registry > */ > @Override > public void addFormatters(FormatterRegistry registry) { > registry.addConverterFactory(stringToBaseEnumConverterFactory); > } > } > ``` #### 2.TypeHandler枚举类型转换 @EnumValue Mybatis预置的**`TypeHandler`**可以处理常用的数据类型转换,例如`String`、`Integer`、`Date`等等,其中也包含枚举类型,但是枚举类型的默认转换规则是**枚举对象实例**(ItemType.APARTMENT)和**实例名称**("APARTMENT")相互映射。若想实现`code`属性到枚举对象实例的相互映射,需要**自定义`TypeHandler`**。不过MybatisPlus提供了一个[通用的处理枚举类型的TypeHandler](https://baomidou.com/guides/auto-convert-enum/)。其使用十分简单,只需在`ItemType`枚举类的`code`属性上增加一个注解**`@EnumValue`**,Mybatis-Plus便可完成从`ItemType`对象到`code`属性之间的相互映射,具体配置如下。 ```java public enum ItemType { APARTMENT(1, "公寓"), ROOM(2, "房间"); @EnumValue private Integer code; private String name; ItemType(Integer code, String name) { this.code = code; this.name = name; } } ``` > 如果不进行配置会出现的情况: > > ![image-20240717112036475](images/README.assets/image-20240717112036475.png) #### **3.HTTPMessageConverter枚举类型转换 @JsonValue** `HttpMessageConverter`依赖于Json序列化框架(默认使用Jackson)。其对枚举类型的默认处理规则也是枚举对象实例(ItemType.APARTMENT)和实例名称("APARTMENT")相互映射。不过其提供了一个注解`@JsonValue`,同样只需在`ItemType`枚举类的`code`属性上增加一个注解`@JsonValue`,Jackson便可完成从`ItemType`对象到`code`属性之间的互相映射。具体配置如下,详细信息可参考Jackson[官方文档](https://fasterxml.github.io/jackson-annotations/javadoc/2.8/com/fasterxml/jackson/annotation/JsonValue.html)。 ```java public enum ItemType { APARTMENT(1, "公寓"), ROOM(2, "房间"); @EnumValue @JsonValue private Integer code; private String name; ItemType(Integer code, String name) { this.code = code; this.name = name; } } ``` > 如果不进行配置会出现的情况: > > image-20240717111926384 ### 9.mybatis中自定义映射规则`` resultMap是MyBatis中的一个配置元素,通过它可以实现数据库查询结果到Java对象的灵活映射。它允许开发者明确指定数据库表中的列如何映射到Java对象的属性上,从而解决数据库字段名和Java对象属性名不一致的问题。 **使用场景** - **数据库字段名和Java对象属性名不一致:**当数据库表中的列名和Java对象的属性名不一致时,可以使用resultMap进行映射。 - **复杂查询结果映射:**在进行关联查询时,查询结果可能包含多个表的数据,此时可以使用resultMap将查询结果映射到复杂的Java对象上。 - **需要进行类型转换:**数据库中的数据类型与Java对象中的数据类型不一致时,可以在resultMap中进行类型转换。(TypeHandler) **实例:** > 如下我们在进行查询时,我们查询结果与我们的Java对象无法直接通过默认的resultType进行一对一映射(resultType只能进行简单的一对一映射),我们此时需要自定义resultMap进行映射 > > **对于其中能够完成自动映射的字段我们也可以通过在resultMap中添加autoMapping="true"** ```xml ``` **注意:如果我们的数据表都使用了逻辑删除,我们在进行两表的left操作(即左连接)的时候,我们的`where`过滤条件只能包含主表,也就是左表的逻辑删除的过滤条件,因为这样才能够保证最终的结果当中包含左表的全部数据,右表的逻辑删除过滤条件要放在`on`连接条件当中(这样我们在执行join操作时,右表当中只有未删除的数据才能跟左表关联上)** ### 10.Spring文件上传接口`MultipartFile` `MultipartFile`是Spring框架中用于处理文件上传的核心接口。当你在Web应用程序中需要接收前端上传的文件时,`MultipartFile`可以非常方便地处理这种场景。这个接口位于`org.springframework.web.multipart`包下,它设计用于封装HTML表单提交的文件数据 **`MultipartFile`的主要功能包括:** - **读取文件内容**:可以通过`getBytes()`方法获取文件的字节内容,或者使用`getInputStream()`方法获取输入流。 - **获取文件元数据**:可以获取文件的名称(`getOriginalFilename()`)、大小(`getSize()`)、MIME类型(`getContentType()`)等信息。 - **文件转移和保存**:可以将上传的文件转移到磁盘上的另一个位置(`transferTo(File dest)`方法) - **错误处理**:当上传过程中发生错误时,`MultipartFile`可以抛出异常,如`IOException`。 在Spring MVC控制器中,你可以直接在方法参数中声明`MultipartFile`类型的参数,Spring会自动绑定上传的文件数据。例如: ```java @PostMapping("/upload") public String handleFileUpload(@RequestParam("file") MultipartFile file, Model model) { if (!file.isEmpty()) { try { // 保存文件到服务器 File serverFile = new File("/path/to/upload/" + file.getOriginalFilename()); file.transferTo(serverFile); model.addAttribute("message", "文件已成功上传"); } catch (IOException e) { model.addAttribute("message", "文件上传失败: " + e.getMessage()); } } else { model.addAttribute("message", "未检测到文件"); } return "result"; } ``` 在这个例子中,`@RequestParam("file")`注解告诉Spring框架,这个参数应该从名为`file`的表单字段中获取数据。 需要注意的是,要使用`MultipartFile`,你的Spring项目必须配置了MultipartResolver。默认情况下,Spring Boot会自动配置一个MultipartResolver,但在非Spring Boot项目中,你可能需要手动配置这个bean。此外,你还需要在`application.properties`或`application.yml`中配置文件上传的大小限制等参数。 ### 11.如何将application.yml文件中的参数映射到实体类中 - **1.首先在application.yml文件中配置好我们需要的参数:** ```xml minio: endpoint: ${LA.minio.endpoint} access-key: ${LA.minio.access-key} secret-key: ${LA.minio.secret-key} bucket-name: ${LA.minio.bucket-name} ``` - **2.两种方式:** - **第一种:** 直接通过@Value注解映射,此处的映射规则比较宽松,虽然我们在application.yml文件中以`-`命名,但依然可以映射为驼峰命名 ```java @Configuration public class MinioConfiguration { @Value("${minio.endpoint}") private String endpoint; @Value("${minio.access-key}") private String accessKey; @Value("${minio.secret-key}") private String secretKey; @Value("${minio.bucket-name}") private String bucketName; } ``` - **第二种:** 1.首先配置单独的实体类用来声明这些参数 ```java @Data @ConfigurationProperties(prefix = "minio") //指定当前类需要绑定的参数名的前缀 public class MinioProperties { private String endpoint; private String accessKey; private String secretKey; private String bucketName; } ``` 2.需要在Spring中注册 ![image-20240718150832781](images/README.assets/image-20240718150832781.png) 两种方式: ```java //第一种 @Configuration @EnableConfigurationProperties(MinioProperties.class) //传入要注册的或者要启用的properties类的class对象 public class MinioConfiguration { } ``` ```java //第二种 @Configuration @ConfigurationPropertiesScan("com.LA_houseLease.common.minio") //指定要注册或者要启用的properties类的包路径 public class MinioConfiguration { } ``` - **3.之后就可以直接通过SpringBoot的自动注入去使用啦** ```java @Configuration @EnableConfigurationProperties(MinioProperties.class) public class MinioConfiguration { @Autowired private MinioProperties minioProperties; } ``` ### 12.全局异常控制器 全局异常控制器,也称为全局异常处理器或异常拦截器,是一个程序组件,用于捕获和处理在应用程序中发生的未处理异常。 > **主要作用:** > > - **集中异常处理**:通过定义一个统一的异常处理方法,在系统任何地方发生异常时都会被调用,避免了在每个可能抛出异常的地方都进行单独处理的繁琐。 > - **提高代码复用性和可维护性**:通过集中处理异常,减少了重复代码,提高了代码的复用性和可维护性。 > - **提供友好错误信息**:可以自定义异常处理逻辑,向用户返回更加友好和易于理解的错误信息,而不是暴露堆栈跟踪等内部信息。 > 使用SpringMVC提供的**全局异常处理**功能 > > **实现方式:** > > ```java > @ControllerAdvice > public class GlobalExceptionHandler { > > @ExceptionHandler(Exception.class) > @ResponseBody > public Result error(Exception e){ > e.printStackTrace(); > return Result.fail(); > } > } > ``` > > 也可以选择直接在类上使用 `@ResponseBody`与`@ControllerAdvice`结合的`@RestControllerAdvice` > > 上述代码中的关键注解的作用如下`@ControllerAdvice`用于声明处理全局Controller方法异常的类`@ExceptionHandler`用于声明处理异常的方法,`value`属性用于声明该方法处理的异常类型`@ResponseBody`表示将方法的返回值作为HTTP的响应体**注意:**全局异常处理功能由SpringMVC提供,因此我们需要引入下面的依赖 > > ```xml > > > org.springframework.boot > spring-boot-starter-web > > ``` > > 实现了全局异常处理器后就可以将项目当中遇到的异常直接抛出,全局异常处理器会进行捕获并统一处理 ### 13.对象类型请求参数扁平化(小细节) 默认情况下Knife4j为该接口生成的接口文档如下图所示,其中的对象类型的参数可能不方便调试 image-20240719110214736 我们可以在application.yml文件中增加如下配置,将queryVo做打平处理 > ```yaml > #生成接口文档时,将对象类型的请求参数扁平化 > #换句话说,如果请求中包含了一个对象类型的参数,并且这个对象有多个属性,那么这些属性将会被直接作为独立的请求参数出现在接口文档中,而不是作为一个整体的对象。 > #方便我们进行调试 > springdoc: > default-flat-param-object: true > ``` image-20240719110325602 ### 14.自定义异常类 ​ java中的异常分为两类:编译时异常、运行时异常 ​ **1.自定义异常处理类** ​ 创建一个继承自Java标准异常类(如`Exception`或`RuntimeException`)的自定义异常类。 ```java @Data public class houseLeaseException extends RuntimeException{ //异常状态码 private Integer code; //message继承自父类RuntimeException //因为原本的RuntimeException中参数只可以填写message,因此我们通过自定义异常对功能进行拓展 /** * 通过状态码和错误消息创建异常对象 * @param code * @param message */ public houseLeaseException(Integer code,String message){ super(message); this.code=code; } /** * 根据响应结果枚举对象创建异常对象 * @param resultCodeEnum */ public houseLeaseException(ResultCodeEnum resultCodeEnum){ super(resultCodeEnum.getMessage()); this.code=resultCodeEnum.getCode(); } } ``` ​ **2.在全局异常控制器中添加自定义异常处理逻辑** ```java @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public Result handler(Exception e){ log.warn("全局异常控制器...........异常信息:{}",e.getMessage()); e.printStackTrace(); return Result.fail(); } @ExceptionHandler(houseLeaseException.class) public Result handler(houseLeaseException e){ log.warn("houseLeaseException异常控制器...........状态码:{},异常信息:{}",e.getCode(),e.getMessage()); e.printStackTrace(); return Result.fail(e.getCode(),e.getMessage()); } } ``` ​ **3.如果需要还需要在Result中添加新的构造方法** ​ **4.可以直接抛出自定义异常,并传入参数** image-20240719161156872 ### 15.Json默认序列化器Jackson设置Data时间格式 后端Data类型数据在返回前段时,经过序列化会出现如下问题 image-20240720104422295 `Date`类型的字段在序列化成JSON字符串时,需要考虑两个点,分别是**格式**和**时区**。本项目使用JSON序列化框架为Jackson,我们需要对格式和时区进行配置 **推荐格式按照字段单独配置,时区全局配置。** 具体配置如下: - **格式**可以按照字段**单独配置**,也可以**全局配置** - **单独配置:** 在指定字段增加`@JsonFormat`注解,如下 ```java @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date appointmentTime; ``` - **全局配置:** 在`application.yml`中增加如下内容 ```yaml spring: jackson: date-format: yyyy-MM-dd HH:mm:ss ``` - **时区**同样可按照字段**单独配置**,也可**全局配置** - **单独配置:** 在指定字段增加`@JsonFormat`注解 ```java @JsonFormat(timezone = "GMT+8") private Date appointmentTime; ``` - **全局配置:** 在`application.yml`中增加如下内容 ```yaml spring: jackson: time-zone: GMT+8 ``` ### 16.定时任务`@Scheduled` > `@Scheduled` 是 Spring 框架中用于定义定时任务的注解,它使得开发者能够非常方便地在应用程序中创建周期性执行的任务。这个注解通常与 Spring 的任务执行和调度框架(Spring Task Scheduling)一起使用,该框架支持基于固定延迟、固定速率以及 cron 表达式的任务调度。 1. **启用定时任务的支持** 在Spring 配置类(启动类)(或主应用类)上添加 `@EnableScheduling` 注解: ```java @SpringBootApplication @ComponentScan("com.LA_houseLease") @EnableScheduling public class AdminApplication { public static void main(String[] args) { SpringApplication.run(AdminApplication.class,args); } } ``` 2. **创建定时任务** 你可以在任何 Spring 管理的 Bean 上使用 `@Scheduled` 注解来定义一个定时任务。你可以指定任务的执行频率或时间。 ```java @Component public class ScheduledTasks { @Scheduled(fixedRate = 5000) // 每5秒执行一次 public void reportCurrentTime() { // 定时任务逻辑 System.out.println("当前时间:" + System.currentTimeMillis()); } // 也可以使用 cron 表达式 @Scheduled(cron = "0/5 * * * * ?") // 每5秒执行一次,与 fixedRate = 5000 效果相同 public void reportCurrentTimeWithCron() { System.out.println("使用cron表达式报告当前时间:" + System.currentTimeMillis()); } } ``` > **注意事项:** > > 1. **线程池**:默认情况下,Spring 会使用一个单线程的 `TaskScheduler` 来执行所有 `@Scheduled` 注解标记的任务。这意味着如果某个任务执行时间较长,那么它将会阻塞其他任务的执行。你可以通过实现 `SchedulingConfigurer` 接口来配置自己的 `TaskScheduler`,使用线程池来避免这个问题。 > 2. **任务执行时间**:当使用 `fixedRate` 属性时,任务的执行间隔时间是固定的,不论任务执行的时间长短。而 `fixedDelay` 属性则是从任务执行完成开始计算延迟时间,再执行下一次任务。 > 3. **cron 表达式**:cron 表达式提供了一种强大的方式来指定任务的执行时间。它由六或七个空格分隔的时间字段组成,分别代表秒、分、时、日、月、星期和年(可选)。 > 4. **异常处理**:定时任务中抛出的未捕获异常默认会记录在日志中,但不会影响其他定时任务的执行。然而,你应该在任务中妥善处理异常,避免潜在的问题。 > 5. **动态调整**:`@Scheduled` 注解在运行时是不支持动态调整的。如果你需要动态地改变任务的执行频率或时间,你可能需要寻找其他解决方案,如使用 Spring 的 `TaskScheduler` API 来编程式地安排任务。 ### 17.密码处理 - 用户的密码通常不会直接以明文的形式保存到数据库中,而是会先经过处理,然后将处理之后得到的"密文"保存到数据库,这样能够降低数据库泄漏导致的用户账号安全问题。密码通常会使用一些单向函数进行处理,如下图所示 ![image-20240720164929809](images/README.assets/image-20240720164929809.png) 常用于处理密码的单向函数(算法)有**MD5**、**SHA-256**等,**Apache Commons**提供了一个工具类`DigestUtils`,其中就包含上述算法的实现。 > **Apache Commons**是Apache软件基金会下的一个项目,其致力于提供可重用的开源软件,其中包含了很多易于使用的现成工具。 使用该工具类需引入`commons-codec`依赖,在**common模块**的pom.xml中增加如下内容 ```xml commons-codec commons-codec ``` ### 18.**Mybatis-Plus更新策略Mybatis-Plus update strategy** 使用Mybatis-Plus提供的更新方法时,若实体中的字段为`null`,默认情况下,最终生成的update语句中,不会包含该字段。若想改变默认行为,可做以下配置。 - 全局配置在`application.yml`中配置如下参数 ```yaml mybatis-plus: global-config: db-config: update-strategy: ``` **注**:上述``可选值有:`ignore`、`not_null`、`not_empty`、`never`,默认值为`not_null` - `ignore`:忽略空值判断,不管字段是否为空,都会进行更新 - `not_null`:进行非空判断,字段非空才会进行判断 - `not_empty`:进行非空判断,并进行非空串("")判断,主要针对字符串类型 - `never`:从不进行更新,不管该字段为何值,都不更新 - 局部配置在实体类中的具体字段通过`@TableField`注解进行配置,如下: ```java @Schema(description = "密码") @TableField(value = "password", updateStrategy = FieldStrategy.NOT_EMPTY) private String password; ``` ### 19.登录认证方案:Session、Token 有两种常见的认证方案,分别是基于**Session**的认证和基于**Token**的认证,下面逐一进行介绍 - **基于Session:** **认证流程如下:** image-20240720203507265 **特点:** - 登录用户信息保存在服务端内存中,若访问量增加,单台节点压力会较大 - 随用户规模增大,若后台升级为集群,则需要解决集群中各服务器登录状态共享的问题。(可以通过将Session对象统一存储于Redis中解决服务器登录状态共享问题) - **基于Token:** **认证流程如下:** image-20240720204218911 **该方案的特点** - 登录状态保存在客户端,服务器没有存储开销 - 客户端发起的每个请求自身均携带登录状态,所以即使后台为集群,也不会面临登录状态共享的问题。 > 我们所说的Token,通常指JWT(JSON Web TOKEN)。JWT是一种轻量级的安全传输方式,用于在两个实体之间传递信息,通常用于身份验证和信息传递。 > > JWT是一个字符串,如下图所示,该字符串由三部分组成,三部分由`.`分隔。三个部分分别被称为 > > - `header`(头部) > > - `payload`(负载) > > - `signature`(签名) > - image-20240720203815664 **各部分的作用如下:** - **Header(头部)**Header部分是由一个JSON对象经过`base64url`编码得到的,这个JSON对象用于保存JWT 的类型(`typ`)、签名算法(`alg`)等元信息,例如 ```json { "alg": "HS256", "typ": "JWT" } ``` - **Payload(负载)**也称为 Claims(声明),也是由一个JSON对象经过`base64url`编码得到的,用于保存要传递的具体信息。JWT规范定义了7个官方字段,如下: - - iss (issuer):签发人 - - exp (expiration time):过期时间 - - sub (subject):主题 - - aud (audience):受众 - - nbf (Not Before):生效时间 - - iat (Issued At):签发时间 - - jti (JWT ID):编号 除此之外,我们还可以自定义任何字段,例如 ```json { "sub": "1234567890", "name": "John Doe", "iat": 1516239022 } ``` - **Signature(签名)**由头部、负载和秘钥一起经过(header中指定的签名算法)计算得到的一个字符串,用于防止消息被篡改。 ### 20.JWT使用 ​ [JWT官网](https://github.com/jwtk/jjwt) - **引入Maven依赖** ```xml io.jsonwebtoken jjwt-api 0.12.6 io.jsonwebtoken jjwt-impl 0.12.6 runtime io.jsonwebtoken jjwt-jackson 0.12.6 runtime ``` - **创建JWT工具类** ```java /** * JWT工具类 */ public class JwtUtil { /** * 生成JWT * @param secretKey 密钥 * @param Ttl 过期时间(毫秒) * @param userId 用户id * @param username 用户名 * @return */ public static String creatToken(String secretKey,Long Ttl,Long userId,String username){ //生成密钥 SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes()); //生成JWT时间 long expMillis = System.currentTimeMillis() + Ttl; Date exp=new Date(expMillis); String jwt = Jwts.builder() .setExpiration(exp) //设置过期时间 .signWith(SignatureAlgorithm.HS256, key) //设置签名所使用的签名算法和签名使用的密钥 .claim("userId", userId) //自定义内容 .claim("username", username) .compact(); return jwt; } /** * 解析token * @param secretKey * @param token */ public static void parseJWT(String secretKey,String token){ //生成密钥 SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes()); //如果token为空说明用户没有登录 if(token==null){ throw new houseLeaseException(ResultCodeEnum.ADMIN_LOGIN_AUTH); } //根据解析过程是否抛异常来校验token是否合法 try{ //解析出来的就是我们的payload Jws claimsJws = Jwts.parserBuilder() .setSigningKey(key) //传入之前加密的密钥(会根据header,payload以及这个密钥重新加密与signature进行验证,来校验是否合法) .build() //构建解析器 .parseClaimsJws(token);// 解析token }catch (ExpiredJwtException e){ throw new houseLeaseException(ResultCodeEnum.TOKEN_EXPIRED);//token过期异常 }catch (JwtException e){ throw new houseLeaseException(ResultCodeEnum.TOKEN_INVALID); } } } ``` - **自定义拦截器** 对发送过来的请求进行拦截,进行身份校验 ```java /** * springMVC中提供的拦截器 * 实现了三种类型拦截器:preHandle(..)、postHandle(..)、afterCompletion(..) * Handler指的其实就是controller */ //身份校验拦截器 @Component public class AuthenticationInterceptor implements HandlerInterceptor { @Autowired private JwtProperties jwtProperties; //返回值为true则放行这个请求 为false则终止 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("access-token"); JwtUtil.parseJWT(jwtProperties.getSecretKey(),token); //如果上一步能走完就说明校验没问题,我们下面直接返回true就可以 如果上面有问题机会直接在解密的方法里抛异常,走不到下一步 return true; } } ``` - **在WebMvcConfiguration中注册自定义拦截器** ```java /** * 向SpringMVC注册自定义配置类 */ @Configuration @Slf4j /** * WebMvcConfigurer这个接口是由springmvc专门提供的一个用于自定义配置的接口, * 只要我们实现了这个接口,就可以去完成各项与springmvc相关的配置 * (比如注册自定义converter以及注册为自定义拦截器等等等) */ public class WebMvcConfiguration implements WebMvcConfigurer { @Autowired private AuthenticationInterceptor authenticationInterceptor; /** * 注册身份校验拦截器 * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { log.warn("开始注册身份校验拦截器............."); registry.addInterceptor(authenticationInterceptor) .addPathPatterns("/admin/**") .excludePathPatterns("/admin/login/**"); } } ``` ### 21.使用ThreadLocal存储登录用户信息 > 一次请求就是一个线程,我么可以把用户的信息存放在线程中,这样这个请求中的每一个处理逻辑都可以从线程中获取用户信息 > > **ThreadLocal概述** > > ThreadLocal的主要作用是为每个使用它的线程提供一个独立的变量副本,使每个线程都可以操作自己的变量,而不会互相干扰,其用法如下图所示。 > > image-20240721142137095 - **创建ThreadLocal工具类** ```java /** * ThreadLocal工具类 * 封装啦ThreadLocal的常用方法 * context:上下文 */ public class BaseContext { public static ThreadLocal threadLocal=new ThreadLocal<>(); public static void setLoginUser(LoginUser loginUser){ threadLocal.set(loginUser); } public static LoginUser getLoginUser(){ return threadLocal.get(); } public static void clear(){ threadLocal.remove(); } } ``` - **创建传递参数的实体(单个参数也可以不创建)** ```java @Data @AllArgsConstructor //生成构造函数 public class LoginUser { private Long userId; private String username; } ``` - **配置拦截器** 需要在原有拦截器的基础上,添加将token解析出的信息传入线程的操作 并需要使用afterCompletion(..)拦截器对完成的请求的线程进行clear操作 ```java /** * springMVC中提供的拦截器 * 实现了三种类型拦截器:preHandle(..)、postHandle(..)、afterCompletion(..) * Handler指的其实就是controller */ //身份校验拦截器 @Component public class AuthenticationInterceptor implements HandlerInterceptor { @Autowired private JwtProperties jwtProperties; //返回值为true则放行这个请求 为false则终止 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("access-token"); Claims claims = JwtUtil.parseJWT(jwtProperties.getSecretKey(), token); Long userId = claims.get("userId", Long.class); String username = claims.get("username", String.class); BaseContext.setLoginUser(new LoginUser(userId,username)); //如果上一步能走完就说明校验没问题,我们下面直接返回true就可以 如果上面有问题机会直接在解密的方法里抛异常,走不到下一步 return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //清理线程池 BaseContext.clear(); } ``` 后续就可以直接在线程中获取用户的信息 ### 22.阿里云短信服务 - **配置所需依赖** ```java com.aliyun dysmsapi20170525 ``` - **配置发送短信客户端** - 在application.yml中添加阿里云短信服务相关参数 ```yaml #阿里云短信服务参数 aliyun: sms: access-key-id: ${LA.aliyun.sms.access-key-id} access-key-secret: ${LA.aliyun.sms.access-key-secret} endpoint: dysmsapi.aliyuncs.com ``` - 创建短信参数实体类 ```java @Data @ConfigurationProperties(prefix = "aliyun.sms") @Component public class AliyunSMSProperties { private String accessKeyId; private String accessKeySecret; private String endpoint; } ``` - 编写配置类 ```java @Slf4j @Configuration //只有当aliyun.sms.endpoint属性存在时,该配置类才会生效(如果不配置的话,admin模块启动扫描到会创建Client但是找不到所需参数会报错) @ConditionalOnProperty(name = "aliyun.sms.endpoint") public class AliyunSmsConfiguration { @Autowired private AliyunSMSProperties aliyunSMSProperties; @Bean public Client creatClient(){ log.warn("aliyunSmsClient创建..............."); Config config = new Config(); config.setAccessKeyId(aliyunSMSProperties.getAccessKeyId()); config.setAccessKeySecret(aliyunSMSProperties.getAccessKeySecret()); config.setEndpoint(aliyunSMSProperties.getEndpoint()); try { return new Client(config); } catch (Exception e) { throw new RuntimeException(e); } } } ``` - 使用 ```java @Service public class SmsServiceImpl implements SmsService { @Autowired private Client client; /** * 发送短信验证码 * @param phone * @param code */ @Override public void sendSMS(String phone, String code) { SendSmsRequest request = new SendSmsRequest(); request.setPhoneNumbers(phone); //设置接收的手机号 request.setSignName("阿里云短信测试"); //设置签名名称 request.setTemplateCode("SMS_154950909"); //设置模板编码 request.setTemplateParam("{\"code\":\""+ code + "\"}"); //传入模板参数 try { client.sendSms(request); } catch (Exception e) { throw new RuntimeException(e); } } } ``` ### 23.SpringBoot的异步操作 SpringBoot提供了**`@Async`**注解来完成异步操作,我们可以直接在我们希望异步操作的方法上添加**`@Async`**注解,并在SpringBoot启动类上添加启用异步操作支持的注解**`@EnableAsync`**即可 > ```java > @Override > @Async > public void saveHistory(Long userId, Long id) { > 。。。 > } > ``` > > ```java > @SpringBootApplication > @ComponentScan("com.LA_houseLease") > @EnableAsync > public class AppApplication { > public static void main(String[] args) { > SpringApplication.run(AppApplication.class); > } > } > ``` ### 24.利用Redis进行缓存 > 缓存优化是一个性价比很高的优化手段,多数情况下,缓存优化可以通过一些简单的操作,换来性能的大幅提升。缓存优化的核心思想就是将一些原本保存在磁盘(例如MySQL)中的、经常访问并且查询开销比较大的数据,临时保存到内存(例如Redis)中。后序再访问相同数据时,就可直接从内存中获取结果,而无需再访问磁盘,由于内存的读写速度远高于磁盘,因此就能极大的提高程序的性能。 > > image-20240722165646747 > > 在使用缓存优化时,有一个问题不得不提,那就是**数据库和缓存数据的一致性**,当数据库中的数据发生变化时,缓存中的数据也要同步更新,否则就会出现数据不一致的问题,解决该问题的方案有如下几个 > > - 数据发生变化时,更新数据库的同时也更新缓存 > > - 数据发生变化时,更新数据库的同时删除缓存 > > 在了解了缓存优化的核心思想后,我们以移动端中的`根据ID获取房间详情`接口为例,进行缓存优化。该接口涉及多表查询,查询时会多次访问数据库,查询代价较高,故可采取缓存优化,加快查询速度。 **1.自定义RedisTemplate** 使用Reids保存缓存数据,因此我们需要使用RedisTemplate进行读写操作。`Spring-data-redis`提供了`StringRedisTemplate`和`RedisTemplate`两个实例,但是两个实例均不满足我们当前的需求,所以我们需要自定义RedisTemplate。 创建RedisConfiguration 做如下配置: ```java /** * 自定义redisTemplate(用于做向redis中保存缓存数据以提高性能) * Spring-data-redis提供了StringRedisTemplate和RedisTemplate两个实例, * 但是两个实例均不满足我们当前的需求,所以我们需要自定义RedisTemplate */ @Configuration public class RedisConfiguration { @Bean public RedisTemplate stringObjectRedisTemplate(RedisConnectionFactory redisConnectionFactory){ RedisTemplate redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); //设置redisTemplate使用的连接工厂 redisTemplate.setKeySerializer(RedisSerializer.string()); //设置键值对键和值的序列化方式 redisTemplate.setValueSerializer(RedisSerializer.java()); return redisTemplate; } } ``` **2.编写缓存逻辑** 需要使用时可以直接用`@Autowired`自动注入即可 ```java @Override public RoomDetailVo getDetailById(Long id) { String key = RedisConstant.APP_ROOM_PREFIX + id; RoomDetailVo roomDetailVo = (RoomDetailVo) redisTemplate.opsForValue().get(key); if (roomDetailVo == null) { //1.查询房间信息 //2.查询图片 //3.查询租期 //4.查询配套 //5.查询标签 ...... roomDetailVo = new RoomDetailVo(); ...... redisTemplate.opsForValue().set(key, roomDetailVo); } //10.保存浏览历史 browsingHistoryService.saveHistory(LoginUserHolder.getLoginUser().getUserId(), id); return roomDetailVo; } ``` **3.编写删除缓存逻辑** 为保证缓存数据的一致性,在房间信息发生变化时,需要删除相关缓存。 ```java @Override public void removeRoomById(Long id) { //1.删除RoomInfo ...... //2.删除graphInfoList ...... //3.删除attrValueList ...... //4.删除facilityInfoList ..... //5.删除labelInfoList ...... //6.删除paymentTypeList ...... //7.删除leaseTermList ...... //8.删除缓存 redisTemplate.delete(RedisConstant.APP_ROOM_PREFIX + id); } ``` ## 项目部署: mysql,minio,redis使用之前单独安装过配置好的,后端项目都直接使用的服务器上的这些基础组件 统一加入网络LA_001 ![image-20240724144034349](images/README.assets/image-20240724144034349.png) ### 1.后端部署 #### 1.1准备工作 (1)创建专用目录 ```shell #创建后端admin服务目录 mkdir house_lease/admin_server -p #创建后端app服务目录 mkdir house_lease/app_server -p ``` (2)创建后端配置文件 项目中虽然打包了配置文件,但是难免会需要修改,我们在此处创建的配置文件的优先级大于打包的jar包中的 ```shell #后端admin服务 cd house_lease/admin_server vim application.yml #后端app服务 cd house_lease/app_server vim application.yml ``` > application.yml使用项目中的就可以,在创建网络后,可以直接使用容器名 (3)后端项目打包 idea ---> maven ---> clean && package ---> server-admin-1.0-SNAPSHOT.jar && server-app-1.0-SNAPSHOT.jar 选择跳过测试,并选中clean package运行 image-20240724144440780 (4)结构 ``` house_lease ├─ admin_server #后端admin服务 | ├─ config #存放配置相关的文件 | | └─ application.yml #挂载映射容器内的application.yml文件 | ├─ logs #挂载映射容器内的日志.log | ├─ Dockerfile #构建镜像的文件 | └─ server-admin-1.0-SNAPSHOT.jar #jar包 ├─ app_server #后端app服务 | ├─ config #存放配置相关的文件 | | └─ application.yml #挂载映射容器内的application.yml文件 | ├─ logs #挂载映射容器内的日志.log | ├─ Dockerfile #构建镜像的文件 | └─ server-app-1.0-SNAPSHOT.jar #jar包 ``` #### 1.2构建脚本 我们需要先将我们的服务构建成镜像 ```dockerfile #指定基础镜像 FROM openjdk:17 #工作目录 WORKDIR /admin_server #拷贝jar包 COPY server-admin-1.0-SNAPSHOT.jar . #暴露后端项目的端口8010 EXPOSE 8010 #默认执行jar(指定容器启动时的默认命令) #CMD ["java","-jar","server-admin-1.0-SNAPSHOT.jar"] ENTRYPOINT java -jar server-admin-1.0-SNAPSHOT.jar --spring.config.location=/config/application.yml ``` ```dockerfile #指定基础镜像 FROM openjdk:17 #工作目录 WORKDIR /app_server #拷贝jar包 COPY server-app-1.0-SNAPSHOT.jar . #暴露后端项目的端口8020 EXPOSE 8020 #默认执行jar(指定容器启动时的默认命令) #CMD ["java","-jar","server-app-1.0-SNAPSHOT.jar"] ENTRYPOINT java -jar server-app-1.0-SNAPSHOT.jar --spring.config.location=/config/application.yml ``` **创建Dockerfile文件** ```yaml /house_lease/admin_server目录下创建Dockerfile文件并粘贴上述命令 /house_lease/app_server目录下创建Dockerfile文件并粘贴上述命令 ``` #### 1.3构建镜像 基于上述Dockerfile文件构建镜像(注意需要在Dockerfile文件所在目录) ``` docker build -t app_server . docker build -t admin_server . ``` #### 1.4运行容器 ``` docker run --name admin_server -d \ -p 8010:8010 \ -v /root/house_lease/admin_server/config:/admin_server/config \ -v /root/house_lease/admin_server/logs:/admin_server/logs \ --network LA_001 \ admin_server ``` ``` docker run --name app_server -d \ -p 8020:8020 \ -v /root/house_lease/app_server/config:/app_server/config \ -v /root/house_lease/app_server/logs:/app_server/logs \ --network LA_001 \ app_server ``` ### 2.前端部署 #### 2.1准备工作 (1)创建专用目录 ```shell #创建前端admin_web目录 mkdir house_lease/admin_web/html -p mkdir house_lease/admin_web/logs -p #创建前端app_web目录 mkdir house_lease/app_web/html -p mkdir house_lease/app_web/logs -p ``` (2)前端项目打包 ``` vscode --> 终端 --> npm run build --> dist 将打包出来的dist文件放到上面创建的html文件下 ``` (3)获取nginx的配置文件 ```shell #运行一个nginx容器 docker run --name nginx -itd nginx #拷贝nginx配置文件 docker cp nginx:/etc/nginx/nginx.conf /root/house_lease/admin_web/nginx.conf docker cp nginx:/etc/nginx/nginx.conf /root/house_lease/app_web/nginx.conf docker cp nginx:/etc/nginx/conf.d /root/house_lease/admin_web/conf.d docker cp nginx:/etc/nginx/conf.d /root/house_lease/app_web/conf.d #移除刚才创建的容器 docker rm -f nginx ``` (4)修改配置文件 ```shell #admin_web vim /root/house_lease/admin_web/conf.d/default.conf server { listen 8011; #指定Nginx服务器监听的端口 server_name ${服务器ip}; #指定虚拟服务器监听的域名 #有当HTTP请求的Host头部与这个指令指定的值匹配时,Nginx才会应用这个server块内的配置来处理请求 location / { #定义URL的匹配规则,以及如何处理这些URL root /root/house_lease/admin_web/html; index index.html; } location /admin { #定义URL的匹配规则,以及如何处理这些URL proxy_pass http://${服务器ip}:8010; #反向代理,将请求转发到后端 } } #app_web vim /root/house_lease/app_web/conf.d/default.conf server { listen 8021; #指定Nginx服务器监听的端口 server_name ${服务器ip}; #指定虚拟服务器监听的域名 #有当HTTP请求的Host头部与这个指令指定的值匹配时,Nginx才会应用这个server块内的配置来处理请求 location / { #定义URL的匹配规则,以及如何处理这些URL root /root/house_lease/admin_web/html; index index.html; } location /app { #定义URL的匹配规则,以及如何处理这些URL proxy_pass http://${服务器ip}:8020; #反向代理,将请求转发到后端 } } ``` (5)结构 ``` house_lease ├─ admin_web #前端admin服务 | ├─ conf.d #挂载映射容器内nginx配置文件 | | └─ default.conf #挂载映射容器内nginx配置文件 | ├─ html #前端打包后的文件 | ├─ logs #挂载映射容器内的日志.log | ├─ Dockerfile #构建镜像的文件 | └─ nginx.conf #挂载映射容器内nginx配置文件 ├─ app_web #前端app服务 | ├─ conf.d #挂载映射容器内nginx配置文件 | | └─ default.conf #挂载映射容器内nginx配置文件 | ├─ html #前端打包后的文件.log | ├─ logs #挂载映射容器内的日志.log | ├─ Dockerfile #构建镜像的文件 | └─ nginx.conf #挂载映射容器内nginx配置文件 ``` #### 2.2构建脚本 我们需要先将我们的服务构建成镜像 ```dockerfile #指定基础镜像 FROM nginx:latest ENV TZ=Asia/Shanghai #拷贝前端文件 COPY ./html /usr/share/nginx/html #暴露前端项目的端口8011 EXPOSE 8011 ``` ```dockerfile #指定基础镜像 FROM nginx:latest ENV TZ=Asia/Shanghai #拷贝前端文件 COPY ./html /usr/share/nginx/html #暴露前端项目的端口8011 EXPOSE 8021 ``` **创建Dockerfile文件** ```yaml /house_lease/admin_web目录下创建Dockerfile文件并粘贴上述命令 /house_lease/app_web目录下创建Dockerfile文件并粘贴上述命令 ``` #### 2.3构建镜像 基于上述Dockerfile文件构建镜像(注意需要在Dockerfile文件所在目录) ``` docker build -t app_web . docker build -t admin_web . ``` #### 2.4运行容器 ```shell docker run --name admin_web -itd \ -p 8011:8011 \ -v /root/house_lease/admin_web/nginx.conf:/etc/nginx/nginx.conf \ -v /root/house_lease/admin_web/conf.d:/etc/nginx/conf.d \ -v /root/house_lease/admin_web/logs:/var/log/nginx \ --network LA_001 \ admin_web ``` ``` docker run --name app_web -itd \ -p 8021:8021 \ -v /root/house_lease/app_web/nginx.conf:/etc/nginx/nginx.conf \ -v /root/house_lease/app_web/conf.d:/etc/nginx/conf.d \ -v /root/house_lease/app_web/logs:/var/log/nginx \ --network LA_001 \ app_web ```