springboot+mybatis+mybatis-plus对crud项目进行改进

news/2025/4/2 6:25:12/

springboot+mybatis实现简单的增、删、查、改https://blog.csdn.net/heyl163_/article/details/132197201上一篇文章,已经详细地介绍了怎么通过springboot项目整合mybatis实现简单的数据库表的增删改查功能,是最简单的springboot项目的结构。所以有很多问题,包括简单sql语句需要重复编写、数据校验、异常处理、操作类方法没有返回响应等等。这篇文章我们通过使用springmvc的全局异常处理机制以及引入mybatis-plus和validation来解决这几个问题。

基于上一篇文章的项目,新建一个分支springboot1.0,保存最新的代码。

一、简单sql语句重复编写问题

通过引入mybatis-plus来解决重复编写简单sql语句的问题。

在pom.xml中引入mybatis-plus的依赖

<properties><mybatis-plus.version>3.5.1</mybatis-plus.version>
</properties><!--mybatis-plus-->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${mybatis-plus.version}</version>
</dependency>

然后SongMapper继承BaseMapper接口,然后就能调用BaseMapper里预先定义的crud方法了。

package com.example.springboot.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.springboot.entity.Song;
import org.springframework.stereotype.Repository;/*** @author heyunlin* @version 1.0*/
@Repository
public interface SongMapper extends BaseMapper<Song> {}

BaseMapper的参数类型为对应实体类的类型;

 

因为BaseMapper里有几个方法selectById()、deleteById()、updateById()需要用到表的主键,因此,在实体类上需要通过注解@TableId来标注哪个字段是表的主键;

 

如果数据库表名和实体类的类名不一致,需要通过@TableName注解指明对应的数据库表;

 

如果数据库表名的字段名和实体类的属性名不一致,需要通过@TableField注解指明对应的数据库表的字段;

package com.example.springboot.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;import java.io.Serializable;
import java.time.LocalDateTime;/*** 歌曲* @author heyunlin* @version 1.0*/
@Data
@TableName("song")
public class Song implements Serializable {private static final long serialVersionUID = 18L;@TableId(type = IdType.INPUT)private String id;/*** 歌曲名*/@TableField("name")private String name;/*** 歌手*/@TableField("singer")private String singer;/*** 描述信息*/@TableField("note")private String note;/*** 最后一次修改时间*/@TableField("last_update_time")@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")private LocalDateTime lastUpdateTime;
}

然后,删除SongMapper.java中编写的方法和对应SongMapper.xml中的statement

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.springboot.mapper.SongMapper"></mapper>

删除之后惊奇地发现service中调用mapperde的方法并没有报错,因为这些方法已经预定义在BaseMapper中了。

mybatis-plus使用简单,归根于BaseMapper中预先帮我们实现的方法,接下来详细介绍这些方法的作用。

package com.baomidou.mybatisplus.core.mapper;import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Param;public interface BaseMapper<T> extends Mapper<T> {// 添加,如果字段值为null,则不会设置到insert语句中int insert(T entity);// 通过id删除,id字段的类型需要实现Serializable接口int deleteById(Serializable id);int deleteById(T entity);// 条件删除,把条件放到Map中,map的key是数据库表的字段名,map的value是字段的值int deleteByMap(@Param("cm") Map<String, Object> columnMap);// 条件删除,通过条件构造器来设置条件int delete(@Param("ew") Wrapper<T> queryWrapper);// 通过ID批量删除,相当于delete from xxx where id in idListint deleteBatchIds(@Param("coll") Collection<?> idList);// 通过id修改,如果字段值为null,则不会设置到update语句中int updateById(@Param("et") T entity);// 条件修改,通过条件构造器来设置条件,entity中的数据为Wrapper的set()方法设置的字段值int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);// 通过id查询,id字段的类型需要实现Serializable接口T selectById(Serializable id);// 通过ID批量查询,相当于select * from xxx where id in idListList<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);// 条件查询,通过条件构造器来设置查询条件List<T> selectByMap(@Param("cm") Map<String, Object> columnMap);// 条件查询,通过条件构造器来设置查询条件// 如果确定查询最多只有一行结果,可以使用该方法,否则将报错default T selectOne(@Param("ew") Wrapper<T> queryWrapper) {List<T> ts = this.selectList(queryWrapper);if (CollectionUtils.isNotEmpty(ts)) {if (ts.size() != 1) {throw ExceptionUtils.mpe("One record is expected, but the query result is multiple records", new Object[0]);} else {return ts.get(0);}} else {return null;}}// existes语句default boolean exists(Wrapper<T> queryWrapper) {Long count = this.selectCount(queryWrapper);return null != count && count > 0L;}// 条件查询记录数,通过条件构造器来设置查询条件Long selectCount(@Param("ew") Wrapper<T> queryWrapper);// 条件查询,通过条件构造器来设置查询条件// 当查询结果有多条时,不能使用selectOne,需要使用该方法List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper);List<Object> selectObjs(@Param("ew") Wrapper<T> queryWrapper);// 分页查询<P extends IPage<T>> P selectPage(P page, @Param("ew") Wrapper<T> queryWrapper);<P extends IPage<Map<String, Object>>> P selectMapsPage(P page, @Param("ew") Wrapper<T> queryWrapper);
}

 

二、数据校验问题

1、ID不能为空的问题

delete()和detail()方法理论上需要ID不为空,否则查询结果必然是空,为了避免无效的查询,应该设置id不能为空,通过@RequestParam(requied = true)来限制id不能为空

SongController.java对应修改

@RequestMapping(value = "/delete/{id}", method = RequestMethod.GET)
public void delete(@RequestParam(required = true) @PathVariable("id") String id) {songService.delete(id);
}
@RequestMapping(value = "/detail/{id}", method = RequestMethod.GET)
public Song detail(@RequestParam(required = true) @PathVariable("id") String id) {return songService.detail(id);
}

如下图,当通过postman测试接口时,不传id会报错

cc8c86b7b65f4c908e0bb4616d3c8f0e.png

传正确的ID时,成功返回了查询结果。

0e911367c75249babdf03f6fe2761d03.png

 

2、添加校验

通过validation数据校验框架完成数据校验。

在pom.xml中引入validation的依赖

<!--validation-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency>

然后创建一个对应前端传来的数据的dto对象,比如添加对应一个dto对象,用户只需要输入name、singer和note三个字段的值,而且name、singer这两个字段是必填的,也就是需要验证非空(ID是通过当前时间生成的)。

10579a2651764c7eb8376c8f57e3b840.png

 添加时的dto对象

package com.example.springboot.dto;import lombok.Data;/*** @author heyunlin* @version 1.0*/
@Data
public class SongInsertDTO {/*** 歌曲名*/private String name;/*** 歌手*/private String singer;/*** 描述信息*/private String note;
}

接着,在字段上使用validation定义的注解对字段值进行校验,校验失败会抛出BindException异常,然后对异常进行处理即可,详情请参考文章:

validation数据校验框架https://blog.csdn.net/heyl163_/article/details/132112153

SongInsertDTO.java

package com.example.springboot.dto;import lombok.Data;import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;/*** @author heyunlin* @version 1.0*/
@Data
public class SongInsertDTO {/*** 歌曲名*/@NotNull(message = "歌曲名不能为空")@NotEmpty(message = "歌曲名不能为空")private String name;/*** 歌手*/@NotNull(message = "歌手不能为空")@NotEmpty(message = "歌手不能为空")private String singer;
}

然后控制器SongController上的insert()方法的参数类型改为SongInsertDTO,然后在参数上添加@Valid或@Validated注解

@RequestMapping(value = "/insert", method = RequestMthod.POST)
public void insert(@Validated SongInsertDTO insertDTO) {
        songService.insert(insertDTO);
}

相应的,service也要修改

SongService

void insert(SongInsertDTO insertDTO);

SongServiceImpl 

@Override
public void insert(SongInsertDTO insertDTO) {Song song = new Song();song.setId(StringUtils.uuid());song.setName(insertDTO.getName());song.setSinger(insertDTO.getSinger());if (StringUtils.isNotEmpty(insertDTO.getNote())) {song.setNote(insertDTO.getNote());}songMapper.insert(song);
}

 

3、修改校验

修改时需要ID、name、singer不为空,则创建一个SongUpdateDTO类即可,然后按照相同的步骤在类的属性上和控制器方法的参数上添加对应的注解即可。

package com.example.springboot.dto;import lombok.Data;import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;/*** @author heyunlin* @version 1.0*/
@Data
public class SongUpdateDTO {/*** 歌曲编号/ID*/@NotNull(message = "歌曲编号不能为空")@NotEmpty(message = "歌曲编号不能为空")private String id;/*** 歌曲名*/@NotNull(message = "歌曲名不能为空")@NotEmpty(message = "歌曲名不能为空")private String name;/*** 歌手*/@NotNull(message = "歌手不能为空")@NotEmpty(message = "歌手不能为空")private String singer;/*** 描述信息*/private String note;
}

控制器SongController上的update()方法的参数类型改为SongUpdateDTO,然后在参数上添加@Valid或@Validated注解 

@RequestMapping(value = "/update", method = RequestMethod.POST)
public void update(@Valid SongUpdateDTO updateDTO) {songService.update(updateDTO);
}

相应的,service也要修改 

SongService

void update(SongUpdateDTO updateDTO);

SongServiceImpl 

@Override
public void update(SongUpdateDTO updateDTO) {Song song = new Song();song.setId(StringUtils.uuid());song.setName(updateDTO.getName());song.setSinger(updateDTO.getSinger());if (StringUtils.isNotEmpty(updateDTO.getNote())) {song.setNote(updateDTO.getNote());}song.setLastUpdateTime(LocalDateTime.now());songMapper.updateById(song);
}

 

三、操作类方法没有响应数据问题

无论操作成功还是失败,都应该向客户端返回操作的结果。

1、创建响应对象

新建一个响应对象实体类,包含状态码、数据和响应的消息(提示)。

package com.example.springboot.restful;import lombok.Data;@Data
public class JsonResult<T> {/*** 响应状态码*/private Integer code;/*** 响应提示信息*/private String message;/*** 响应数据*/private T data;public static JsonResult<Void> success() {return success(null);}public static JsonResult<Void> success(String message) {return success(message, null);}public static <T> JsonResult<T> success(String message, T data) {JsonResult<T> jsonResult = new JsonResult<>();jsonResult.setCode(ResponseCode.OK.getValue());jsonResult.setMessage(message);jsonResult.setData(data);return jsonResult;}public static JsonResult<Void> error(String message) {JsonResult<Void> jsonResult = new JsonResult<>();jsonResult.setCode(ResponseCode.ERROR.getValue());jsonResult.setMessage(message);return jsonResult;}public static JsonResult<Void> error(ResponseCode responseCode, Throwable e) {return error(responseCode, e.getMessage() != null ? e.getMessage() : "系统发生异常,请联系管理员!");}public static JsonResult<Void> error(ResponseCode responseCode, String message) {JsonResult<Void> jsonResult = new JsonResult<>();jsonResult.setCode(responseCode.getValue());jsonResult.setMessage(message);return jsonResult;}}

 

2、定义响应状态码

package com.example.springboot.restful;/*** 响应状态码* @author heyunlin* @version 1.0*/
public enum ResponseCode {/*** 请求成功*/OK(200),/*** 失败的请求*/BAD_REQUEST(400),/*** 未授权*/UNAUTHORIZED(401),/*** 禁止访问*/FORBIDDEN(403),/*** 找不到*/NOT_FOUND(404),/*** 不可访问*/NOT_ACCEPTABLE(406),/*** 冲突*/CONFLICT(409),/*** 服务器发生异常*/ERROR(500);private final Integer value;ResponseCode(Integer value) {this.value = value;}public Integer getValue() {return value;}}

 

3、增删改方法添加返回值

insert()、update()、delete()三个方法的返回值类型修改为JsonResult<Void>,JsonResult<T>的参数类型T表示返回的数据的类型,在这里不需要返回数据,只需要返回给用户看的提示信息。

SongController.java

package com.example.springboot.controller;import com.example.springboot.dto.SongInsertDTO;
import com.example.springboot.dto.SongUpdateDTO;
import com.example.springboot.entity.Song;
import com.example.springboot.restful.JsonResult;
import com.example.springboot.service.SongService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;import javax.validation.Valid;/*** @author heyunlin* @version 1.0*/
@RestController
@RequestMapping(path = "/song", produces="application/json;charset=utf-8")
public class SongController {private final SongService songService;@Autowiredpublic SongController(SongService songService) {this.songService = songService;}@RequestMapping(value = "/insert", method = RequestMethod.POST)public JsonResult<Void> insert(@Validated SongInsertDTO insertDTO) {songService.insert(insertDTO);return JsonResult.success("添加成功");}@RequestMapping(value = "/delete/{id}", method = RequestMethod.GET)public JsonResult<Void> delete(@RequestParam(required = true) @PathVariable("id") String id) {songService.delete(id);return JsonResult.success("删除成功");}@RequestMapping(value = "/update", method = RequestMethod.POST)public JsonResult<Void> update(@Valid SongUpdateDTO updateDTO) {songService.update(updateDTO);return JsonResult.success("修改成功");}@RequestMapping(value = "/detail/{id}", method = RequestMethod.GET)public Song detail(@RequestParam(required = true) @PathVariable("id") String id) {return songService.detail(id);}}

这样的话,用户操作添加、删除、修改歌曲时,能得到响应,成功或者失败,以及失败的原因。

 

4、深入分析操作影响行数 

当然了,这里只要没报错就默认操作成功了,如果要求完美,可以获取BaseMapper的增删改方法的返回值,这个返回值是int类型,表示本次操作受影响的行数:插入行数、删除行数、修改行数,根据这个行数返回对应的提示给用户即可。

对应SongServiceImpl的修改

package com.example.springboot.service.impl;import com.example.springboot.dto.SongInsertDTO;
import com.example.springboot.dto.SongUpdateDTO;
import com.example.springboot.entity.Song;
import com.example.springboot.exception.GlobalException;
import com.example.springboot.mapper.SongMapper;
import com.example.springboot.restful.ResponseCode;
import com.example.springboot.service.SongService;
import com.example.springboot.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.time.LocalDateTime;/*** @author heyunlin* @version 1.0*/
@Service
public class SongServiceImpl implements SongService {private final SongMapper songMapper;@Autowiredpublic SongServiceImpl(SongMapper songMapper) {this.songMapper = songMapper;}@Overridepublic void insert(SongInsertDTO insertDTO) {Song song = new Song();song.setId(StringUtils.uuid());song.setName(insertDTO.getName());song.setSinger(insertDTO.getSinger());if (StringUtils.isNotEmpty(insertDTO.getNote())) {song.setNote(insertDTO.getNote());}int rows = songMapper.insert(song);if (rows < 1) {throw new GlobalException(ResponseCode.BAD_REQUEST, "添加时发生了异常,预计影响" + rows + "条记录,已经对本次操作影响的数据进行恢复");}}@Overridepublic void delete(String id) {int rows = songMapper.deleteById(id);if (rows == 0) {throw new GlobalException(ResponseCode.BAD_REQUEST, "删除失败,本次操作删除了" + rows + "条记录。");} else if (rows > 1) {throw new GlobalException(ResponseCode.BAD_REQUEST, "删除失败,操作过程中发生了不可预知的错误。");}}@Overridepublic void update(SongUpdateDTO updateDTO) {Song song = new Song();song.setId(StringUtils.uuid());song.setName(updateDTO.getName());song.setSinger(updateDTO.getSinger());if (StringUtils.isNotEmpty(updateDTO.getNote())) {song.setNote(updateDTO.getNote());}song.setLastUpdateTime(LocalDateTime.now());int rows = songMapper.updateById(song);if (rows == 0) {throw new GlobalException(ResponseCode.BAD_REQUEST, "修改失败,本次操作删除了" + rows + "条记录。");} else if (rows > 1) {throw new GlobalException(ResponseCode.BAD_REQUEST, "修改失败,操作过程中发生了不可预知的错误。");}}@Overridepublic Song detail(String id) {return songMapper.selectById(id);}}

 

四、异常处理问题

程序的健壮性作为3大重要指标,可见非常重要,当程序发生异常时,应该被很好的处理,而不是让用户看到500的页面,或者长时间无响应。

这篇文章通过springmvc统一异常处理机制来解决这个问题,不需要在每个方法上使用try...catch...finally来手动处理异常。

首先,创建一个自定义异常,继承RuntimeException,这时候可能你就会好奇:为什么是继承RuntimeException呢?能不能继承其他Exception,答案是可以。

但是这里需要了解检查异常和非检查异常的区别:

非检查异常:RuntimeException及其子类异常,不需要手动处理,也就是不需要通过try...catch捕获或者通过throws关键字在方法上面申明抛出异常。

检查异常:除了RuntimeException及其子类型异常以外的异常都是检查异常,必须手动处理,通过try...catch捕获或者通过throws关键字在方法上面申明抛出异常。如果不处理,编译就会报错。

所以,我们使用非检查异常的好处显而易见,当我们在代码里主动使用throw关键字抛出RuntimeException异常时不需要去处理就可以通过编译。

我们创建一个exception子包来存放异常相关的类,在该类下创建一个全局异常类GlobalException

package com.example.springboot.exception;import com.example.springboot.restful.ResponseCode;
import lombok.Data;
import lombok.EqualsAndHashCode;/*** 自定义异常* @author heyunlin* @version 1.0*/
@Data
@EqualsAndHashCode(callSuper = true)
public class GlobalException extends RuntimeException {private ResponseCode responseCode;public GlobalException(ResponseCode responseCode, String message) {super(message);setResponseCode(responseCode);}}

然后在当前的exception包下面创建一个handler,然后在handler包下创建一个统一异常处理类

package com.example.springboot.exception.handler;import com.example.springboot.exception.GlobalException;
import com.example.springboot.restful.JsonResult;
import com.example.springboot.restful.ResponseCode;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;import javax.servlet.http.HttpServletResponse;
import java.util.Objects;/*** 全局异常处理类* @author heyunlin* @version 1.0*/
@RestControllerAdvice
public class GlobalExceptionHandler {/*** 处理GlobalException* @param e GlobalException* @return JsonResult<Void>*/@ExceptionHandler(GlobalException.class)public JsonResult<Void> handlerGlobalException(HttpServletResponse response, GlobalException e) {e.printStackTrace();response.setStatus(e.getResponseCode().getValue());return JsonResult.error(e.getResponseCode(), e);}/*** 处理BindException* @param e BindException* @return JsonResult<Void>*/@ExceptionHandler(BindException.class)@ResponseStatus(HttpStatus.BAD_REQUEST)public JsonResult<Void> handlerBindException(BindException e) {e.printStackTrace();BindingResult bindingResult = e.getBindingResult();FieldError fieldError = bindingResult.getFieldError();String defaultMessage = Objects.requireNonNull(fieldError).getDefaultMessage();return JsonResult.error(ResponseCode.BAD_REQUEST, defaultMessage);}/*** 处理Exception* @param e Exception* @return JsonResult<Void>*/@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public JsonResult<Void> handlerException(Exception e) {e.printStackTrace();return JsonResult.error(ResponseCode.ERROR, e);}}

这样的话,当发生对应的异常时,会执行对应的方法,比如数据校验失败时发生了BindException,会执行以下方法

/**
 * 处理BindException
 * @param e BindException
 * @return JsonResult<Void>
 */
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public JsonResult<Void> handlerBindException(BindException e) {
        e.printStackTrace();

 

        BindingResult bindingResult = e.getBindingResult();
        FieldError fieldError = bindingResult.getFieldError();
        String defaultMessage = Objects.requireNonNull(fieldError).getDefaultMessage();

 

        return JsonResult.error(ResponseCode.BAD_REQUEST, defaultMessage);
}

通过上面几个步骤,项目的结构已经相对完善了。

 

好了,这篇文章就分享到这里了,看完不要忘了点赞+收藏哦~

源码已经上传至git,按需获取:

springboot整合mybatis实现crud项目改进https://gitee.com/he-yunlin/springboot/tree/springboot1.0/

 


http://www.ppmy.cn/news/1025543.html

相关文章

MOCK测试

介绍 mock&#xff1a;就是对于一些难以构造的对象&#xff0c;使用虚拟的技术来实现测试的过程。 mock测试&#xff1a;在测试过程中&#xff0c;对于某些不容易构造或者不容易获取的对象&#xff0c;可以用一个虚拟的对象来代替的测试方 法。 接口Mock测试&#xff1a;在接口…

C++——string的简要模拟实现

对于命名空间std的string类&#xff0c;现在我们手动实现&#xff0c;了解string类运作的大致逻辑。 大体框架 现在对于string需要的成员函数以及成员变量&#xff0c;需要先列出来&#xff0c;方便后边的书写 namespace x {class string{public:string(const char* str &q…

tsup打包如何更改outFileName

首先简单介绍一下&#xff0c;什么是tsup tsup是一个打包工具&#xff0c;类似的有rollup tsup的官网&#xff1a;https://tsup.egoist.dev/ tsup的优点&#xff1a;打包速度快&#xff0c;配置少&#xff0c;与ts兼容良好 安装 npm i tsup -D # Or Yarn yarn add tsup --…

RAM不够?CUBEIDE使用CCMRAM

RAM不够&#xff1f;使用CCMRAM 文章目录 RAM不够&#xff1f;使用CCMRAM打开连接LD文件&#xff1a;添加代码添加标识宏使用 打开连接LD文件&#xff1a; 添加代码 在SECTIONS段最后加上下面代码&#xff1a; _siccmram LOADADDR(.ccmram); /* CCM-RAM section * * IMPORTAN…

Leetcode-每日一题【剑指 Offer 20. 表示数值的字符串】

题目 请实现一个函数用来判断字符串是否表示数值&#xff08;包括整数和小数&#xff09;。 数值&#xff08;按顺序&#xff09;可以分成以下几个部分&#xff1a; 若干空格一个 小数 或者 整数&#xff08;可选&#xff09;一个 e 或 E &#xff0c;后面跟着一个 整数若干空…

AcWing算法提高课-1.3.17背包问题求具体方案

宣传一下算法提高课整理 <— CSDN个人主页&#xff1a;更好的阅读体验 <— 本题链接&#xff08;AcWing&#xff09; 点这里 题目描述 有 N N N 件物品和一个容量是 V V V 的背包。每件物品只能使用一次。 第 i i i 件物品的体积是 v i v_i vi​&#xff0c;价值…

腾讯云服务器地域怎么选?可用区是什么?

腾讯云服务器地域有什么区别&#xff1f;怎么选择比较好&#xff1f;地域选择就近原则&#xff0c;距离地域越近网络延迟越低&#xff0c;速度越快。关于地域的选择还有很多因素&#xff0c;地域节点选择还要考虑到网络延迟速度方面、内网连接、是否需要备案、不同地域价格因素…

Nginx+Tomcat的动静分离

首先准备好5台机子&#xff1a;2台装有tomcat&#xff0c;3台装有nginx 1.关闭5台机子的防火墙 systemctl stop firewalld systemctl disable firewalld setenforce 0 Nginx1 vim /usr/local/nginx/conf/nginx.conf#在--#pid-- 下做四层代理 stream {upstream test {server …