在实际开发中遇到的各种问题解决方案
- 第一问:使用axios异步请求完成数据导出(Excel)(基于hutool工具包)
- 第二问:发送邮件功能
- 第三问:通过JDK8的新特性获取现在到明天凌晨的小时、分钟、秒、毫秒
- 第四问使用EasyExcel完成Excel的导入和导出
- 第五问:SpringBoot项目中,Jackson转换Long、LocalDate、LocalDateTime问题
- 第六问:web项目的统一响应结果
- 第7问:使用阿里云实现OSS文件直传服务
- 第8问:完成数据校验功能
- 第9问:使用MinIO完成签名后直传功能
- 第10问:使用element-ui、EasyExcel、mybatisplus完成Excel的导入导出
- 第11问:Vue配合ElementUI使用过程中的一些小问题
- 第12问:hutool工具类的常用功能
- 在Vue中,有分页的页面的列表如何显示序号
- 第13问:SpringBoot项目瘦身,代码与jar分离打包
- 第14问:如何进行短信验证码的对接
- 第16问: mybatisplus代码生成
- 第17问:SpringMVC使用过程中的问题
- 第18问 Redis + Token解决接口幂等性问题
- 第19问 分布式定时任务调度xxl-job的使用
- 第20问 禅道系统的学习与使用
第一问:使用axios异步请求完成数据导出(Excel)(基于hutool工具包)
(1.1)编写后台接口,获取到response对象以及前端传来的数据,使用@RequestBody获取到需要进行导出的数据id
(1.1.1)引入jar包(hutool工具类实现)
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.8</version>
</dependency>
(1.1.2)编写接口(HttpServletResponse response, @RequestBody List<Long> ids)
@Controller
@RequestMapping("/export")
public class ExcelExportController {
@Autowired
private DeptInfoMapper deptInfoMapper;
@PostMapping("/deptInfo")
public void exportNewsInfoExcel(HttpServletResponse response, @RequestBody List<Long> ids) throws IOException {
// 设置文件导出的一些响应头等
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
response.setHeader("Content-Disposition","attachment;filename=test.xlsx");
List<DeptInfo> list = deptInfoMapper.selectListByIds(ids);
// 通过工具类创建writer
ExcelWriter writer = ExcelUtil.getWriter(true);
// 合并单元格后的标题行,使用默认标题样式,写的4,就代表一共有5列,因为是从0开始
writer.merge(4, "部门信息表");
//自定义标题别名
writer.addHeaderAlias("id", "默认ID");
writer.addHeaderAlias("name", "部门名称");
writer.addHeaderAlias("description", "部门描述");
writer.addHeaderAlias("principal", "部门负责人");
writer.addHeaderAlias("tel", "联系电话");
// 一次性写出内容,使用默认样式,强制输出标题
writer.write(list, true);
// 刷新到响应的输出流中
writer.flush(response.getOutputStream());
// 关闭writer,释放内存
writer.close();
}
}
(1.2)前端界面请求数据导出接口,选择性携带参数
(1.2.1)编写一个方法,专门用户做文件的导出请求
// 发送post请求来模拟下载,需要传递条件, url是请求地址,data是请求的数据,格式为对象,filename为文件名称
postDownLoadFile(url, data, filename = '部门列表.xlsx') {
axios.request({
url: url,
method: 'post', // 以post请求为例
data: data, // 筛选列表的请求参数
responseType: 'blob', // 注意响应的类型,必须为blob类型,是一个原始数据的类文件对象,能够使用二进制或文本读取
}).then(res=>{
// 获取到response中的文件数据
const data = res.data
// 将二进制文件转化为可访问的url
let url = window.URL.createObjectURL(data)
// 使用原生的document操作来创建一个a标签,并添加到body标签中
var a = document.createElement('a')
document.body.appendChild(a)
// 设置其href请求路径
a.href = url
// 设置好下载的文件名,需要带上后缀
a.download = filename
// 模拟点击下载
a.click()
// 清除刚刚使用window.URL.createObjectURL(data) 创建的数据,避免在内存中遗留
window.URL.revokeObjectURL(url)
})
}
(1.2.2)编写一个按钮,设置点击事件,在点击后将已选中的表格id拿到,再拿着数据请求接口即可
// 点击导出按钮时的点击事件
exportExcel() {
// 获取需要获取数据的id列表
let ids = [];
// 获取到现在选择的表格列表
this.$refs.multipleTable.selection.forEach(row => ids.push(row.id));
this.postDownLoadFile('/edusys/export/deptInfo', ids, '部门列表的excel表格.xlsx')
}
第二问:发送邮件功能
2.1 原生发送邮件功能(使用qq邮箱进行演示)
-
打开QQ邮箱,开通SMTP功能



-
创建一个Maven项目,引入如下依赖
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.4.7</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
- 编写Java代码
import com.sun.mail.util.MailSSLSocketFactory;
import org.junit.jupiter.api.Test;
import javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.Properties;
/**
* @author codeStars
* @date 2022/9/30 14:40
*/
public class SendEmailTest {
@Test
public void sendEmail() throws Exception{
//创建一个配置文件并保存
Properties properties = new Properties();
properties.setProperty("mail.host","smtp.qq.com");
properties.setProperty("mail.transport.protocol","smtp");
properties.setProperty("mail.smtp.auth","true");
//QQ存在一个特性设置SSL加密
MailSSLSocketFactory sf = new MailSSLSocketFactory();
sf.setTrustAllHosts(true);
properties.put("mail.smtp.ssl.enable", "true");
properties.put("mail.smtp.ssl.socketFactory", sf);
//创建一个session对象
Session session = Session.getDefaultInstance(properties, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("你的QQ邮箱","你的授权码");
}
});
//开启debug模式
session.setDebug(true);
//获取连接对象
Transport transport = session.getTransport();
//连接服务器
transport.connect("smtp.qq.com","你的QQ邮箱","你的授权码");
//创建邮件对象
MimeMessage mimeMessage = new MimeMessage(session);
//邮件发送人
mimeMessage.setFrom(new InternetAddress("你的QQ邮箱"));
//邮件接收人
mimeMessage.setRecipient(Message.RecipientType.TO,new InternetAddress("邮件的接收者的邮箱"));
//邮件标题
mimeMessage.setSubject("Hello Mail,我是邓");
//邮件内容
mimeMessage.setContent("今天天气真好","text/html;charset=UTF-8");
//发送邮件
transport.sendMessage(mimeMessage,mimeMessage.getAllRecipients());
//关闭连接
transport.close();
}
}
2.2 使用SpringBoot配合邮件启动器完成发送邮件功能
2.2.1 引入依赖
<properties>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
</properties>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2.2.2 在application.yml配置文件中配置一些基本信息
spring:
#邮箱基本配置
mail:
#配置smtp服务主机地址
host: smtp.qq.com
#发送者邮箱
username: xxxxx@qq.com
#配置密码,注意不是真正的密码,而是刚刚申请到的授权码
password: bwzxbgdaeisexxxx
#端口号465或587
port: 587
#默认的邮件编码为UTF-8
default-encoding: UTF-8
#其他参数
properties:
mail:
#配置SSL 加密工厂
smtp:
ssl:
#本地测试,先放开ssl,实际使用需要调整为true
enable: false
required: false
#开启debug模式,这样邮件发送过程的日志会在控制台打印出来,方便排查错误
debug: true

2.2.3 测试发送简单邮件
@SpringBootTest
public class SpringBootSendMailTest {
/**
* 邮件启动器为我们提供的邮件发送器
*/
@Autowired
private JavaMailSenderImpl javaMailSender;
/**
* 发信人,必须为一个邮箱,可以从刚刚在配置文件中进行的配置获取
*/
@Value("${spring.mail.username}")
private String addresser;
@Test
public void sendSimpleMail() {
SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
// 文件标题
simpleMailMessage.setSubject("我是邮件标题aaa");
// 文件内容
simpleMailMessage.setText("我是文件内容");
// 寄件人
simpleMailMessage.setFrom(addresser);
// 收件人
simpleMailMessage.setTo("2095622383@qq.com");
// 设置邮件发送时间,实际上还是立即发送的
// 只是把邮件接收者看到的邮件接收时间往后推了10分钟
LocalDateTime localDateTime = LocalDateTime.now().plusMinutes(10);
long time = Timestamp.valueOf(localDateTime).getTime();
simpleMailMessage.setSentDate(new Date(time));
// 发送邮件
javaMailSender.send(simpleMailMessage);
}
}
2.2.4 测试发送有附件的邮件
@SpringBootTest
public class SpringBootSendMailTest {
/**
* 邮件启动器为我们提供的邮件发送器
*/
@Autowired
private JavaMailSenderImpl javaMailSender;
/**
* 发信人,必须为一个邮箱,可以从刚刚在配置文件中进行的配置获取
*/
@Value("${spring.mail.username}")
private String addresser;
@Test
public void sendFileMail() throws Exception{
// 创建复杂类型的邮件
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
// 后面那个true的作用是设置需要发送附件或html
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true);
// 文件标题
mimeMessageHelper.setSubject("我是邮件标题aaa");
// 文件内容
mimeMessageHelper.setText("我是文件内容");
// 寄件人
mimeMessageHelper.setFrom(addresser);
// 收件人
mimeMessageHelper.setTo("2095622383@qq.com");
// 添加需要发送的附件,并指定文件名称
mimeMessageHelper.addAttachment("可爱图片.png", new File("C:\\Users\\DQX\\Downloads\\可可爱爱小盆栽-20.png"));
// 发送邮件
javaMailSender.send(mimeMessage);
}
}
第三问:通过JDK8的新特性获取现在到明天凌晨的小时、分钟、秒、毫秒
@Test
public void testTime() {
// 获取当前的时间戳
long nowTimeStamp = Instant.now().toEpochMilli();
// 获取一天后的时间戳
long tomorrowTimeStamp = LocalDateTime.now().plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0).toInstant(ZoneOffset.of("+8")).toEpochMilli();
// 现在到明天凌晨的毫秒数
long millisSecond = tomorrowTimeStamp - nowTimeStamp;
System.out.println(millisSecond);
// 现在到明天凌晨的秒数
long second = millisSecond / 1000;
System.out.println(second);
// 现在到明天凌晨的分钟
long minute = second / 60;
System.out.println(minute);
// 现在到明天凌晨的小时
long hour = minute / 60;
System.out.println(hour);
}
补充问题: 通过LocaldateTime获取当前的时间戳
LocalDateTime dateTime = LocalDateTime.of(2023, 11, 24, 9, 0, 0); // 指定日期时间
System.out.println(dateTime);
long timestamp = dateTime.toInstant(ZoneOffset.of("+08:00")).toEpochMilli(); // 转换为时间戳(秒)
System.out.println("DateTime: " + dateTime);
System.out.println("Timestamp: " + timestamp);
第四问使用EasyExcel完成Excel的导入和导出
后端代码的实现
1、引入EasyExcel的依赖(准备工作)
<!--excel导出实现-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.10</version>
</dependency>
2、为需要被导出的实体类,进行相对应的注解配置(准备工作)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DeptInfo implements Serializable {
private static final long serialVersionUID = 294145927557737848L;
/**
* @ExcelIgnore 表示写或读Excel的时候,忽略该字段,若不忽略该字段,则会默认将字段名作为表头
*/
@ExcelIgnore
private Long id;
/**
* @ExcelProperty 设置表头
*/
@ExcelProperty("部门名称")
private String name;
@ExcelProperty("部门描述")
private String description;
@ExcelProperty("部门负责人")
private String principal;
@ExcelProperty("部门电话")
private String tel;
}
导出功能
(1)编写一个控制器,用于接收查询的参数,以及文件的导出
@Autowired
private DeptInfoMapper deptInfoMapper;
/**
* 部门的Excel导出功能
* @param response
* @param ids
* @throws IOException
*/
@PostMapping("/exportExcel")
public void exportNewsInfoExcel2(HttpServletResponse response, @RequestBody List<Long> ids) throws IOException {
// 查询出需要的数据
List<DeptInfo> list = deptInfoMapper.selectListByIds(ids);
// 导出Excel
exportExcel(response, DeptInfo.class, list);
}
(1.1)封装导出Excel的方法
/**
* 抽取导出功能
* @param response 响应对象
* @param clazz 导出的实体类型
* @param list 导出的数据
* @throws IOException
*/
public void exportExcel(HttpServletResponse response, Class clazz, List list) throws IOException {
// 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), clazz).sheet("模板").doWrite(list);
}
导入功能
(1)编写读取Excel时的监听器
@Slf4j
/**
* 该类不能被spring管理
*/
public class DataListener implements ReadListener<DeptInfo> {
/**
* 用于存储每一条数据
*/
private List<DeptInfo> cachedDataList = new ArrayList<>();
/**
* 访问数据库的Mapper对象
*/
private DeptInfoMapper deptInfoMapper;
public DataListener(DeptInfoMapper deptInfoMapper) {
this.deptInfoMapper = deptInfoMapper;
}
/**
* 这个每一条数据解析都会来调用
*/
@Override
public void invoke(DeptInfo deptInfo, AnalysisContext analysisContext) {
// 放入缓存,读取完毕后再一起写入
cachedDataList.add(deptInfo);
}
/**
* 所有数据都读取完成后,则会调用该方法
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 将数据保存到数据库中
saveData();
log.info("所有数据解析完成!");
}
/**
* 参照官方写法,返回true,若返回false则代表没有数据了,会直接读了个寂寞
* @param analysisContext
* @return
*/
@Override
public boolean hasNext(AnalysisContext analysisContext) {
return true;
}
/**
* 加上存储数据库
*/
private void saveData() {
log.info("{}条数据,开始存储数据库!", cachedDataList.size());
deptInfoMapper.insertBatch(cachedDataList);
log.info("存储数据库成功!");
}
@Override
public void invokeHead(Map<Integer, CellData> map, AnalysisContext analysisContext) {}
@Override
public void extra(CellExtra cellExtra, AnalysisContext analysisContext) { }
@Override
public void onException(Exception e, AnalysisContext analysisContext) throws Exception {
log.error("Excel读取出现异常,{}", e.getMessage());
}
}
(1)编写一个控制器,用于接收导入的请求
@PostMapping("/importExcel")
@ResponseBody
public ResponseEntity importExcel(MultipartFile file){
try{
// 导入Excel,将文件流,以及需要对应的实体类,以及读取的监听器传入
importExcel(file.getInputStream(), DeptInfo.class, new DataListener(deptInfoMapper));
}catch (Exception e) {
return ResponseEntity.ok(new ResultMsg(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Excel导入失败,请检查上传的文件格式!"));
}
// 导入成功
return ResponseEntity.ok(new ResultMsg(HttpStatus.OK.value(), "导入成功"));
}
(2)封装导入Excel的方法
/**
* 抽取导入功能
* @param inputStream 需要读取的文件输入流
* @param clazz 读取的实体类型
* @param readListener 读取文件时的监听器
*/
public void importExcel(InputStream inputStream, Class clazz, ReadListener<?> readListener) {
EasyExcel.read(inputStream, clazz, readListener).sheet().doRead();
}
前端代码的实现
(1)导出功能(使用axios实现异步导出功能)
(1.1)添加导出功能的按钮,为该按钮设置一个@click点击事件
<el-button type="success" @click="exportExcel">导出数据为Excel</el-button>
(1.2)编写前端导出事件的核心代码,主要是获取当前已经选中的行
exportExcel() {
// 获取需要获取数据的id列表
let ids = [];
// 获取到现在选择的表格列表
this.$refs.multipleTable.selection.forEach(row => ids.push(row.id));
// 调用自定义的导出方法
this.postDownLoadFile('/edusys/excel/exportExcel', ids, '部门列表的excel表格.xlsx')
}
(1.3)自定义一个导出的方法,模拟下载
postDownLoadFile(url, data, filename = '部门列表.xlsx') {
axios.request({
url: url,
method: 'post', // 以post请求为例
data: data, // 筛选列表的请求参数
responseType: 'blob', // 注意响应的类型,必须为blob类型,是一个原始数据的类文件对象,能够使用二进制或文本读取
}).then(res=>{
// 获取到response中的文件数据
const data = res.data
// 将二进制文件转化为可访问的url
let url = window.URL.createObjectURL(data)
// 使用原生的document操作来创建一个a标签,并添加到body标签中
var a = document.createElement('a')
document.body.appendChild(a)
// 设置其href请求路径
a.href = url
// 设置好下载的文件名,需要带上后缀
a.download = filename
// 模拟点击下载
a.click()
// 清除刚刚使用window.URL.createObjectURL(data) 创建的数据,避免在内存中遗留
window.URL.revokeObjectURL(url)
})
}
(2)导出功能
(2.1)编写一个dialog组件,用于Excel文件上传的框
<!-- 上传excel导入功能 -->
<el-dialog
:close-on-click-modal="false"
title="导入excel"
:visible.sync="dialogImportExcelVisible">
<!--
ref: 该文件上传的组件指定一个ref,这样的话就可以通过 this.$refs.excelUpload的方式获取到该组件,才能调用该组件的方法,例如submit()
class: 这就不说了
action: 文件需要上传到的地址,填写刚刚控制器的地址,为什么要带/edusys上下文呢?因为浏览器识别/的时候,只会识别到http://localhost:80
:on-success: 当文件上传成功时的回调函数
:multiple="false" 禁止选中多个文件
:limit 设置文件的上传数量
:auto-upload="false" 关闭文件上传的自动提交功能
:file-list="importFileList" 会显示在页面中的文件列表
:on-change 当文件列表发生改变时,就会调用该方法
:on-exceed 当文件上传超出limit限制时,就会调用该方法,我这里的limit是1,因此只要上传第二个文件,那么就会调用该方法
-->
<el-upload
ref="excelUpload"
class="upload-excel"
action="/edusys/excel/importExcel"
:on-success="uploadExcelSuccess"
:multiple="false"
:limit="1"
:auto-upload="false"
:file-list="importFileList"
:on-change="handleImportChange"
:on-exceed="excelHandleExceed">
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传xlsx、xls结尾的Excel文件,并且一次只能上传一个!</div>
</el-upload>
<div slot="footer" class="dialog-footer">
<!-- 点击取消时,将该上传的dialog关闭 -->
<el-button @click="dialogImportExcelVisible = false">取 消</el-button>
<!-- 手动上传文件 -->
<el-button type="primary" @click="submitImportExcel">确 定</el-button>
</div>
</el-dialog>
(2.2)在Vue示例的data中添加几个相对应的变量
// 导入时的上传文件列表
importFileList: [],
// 导入excel的弹出框标识
dialogImportExcelVisible: false
(2.3)编写文件列表发生变更时的回调函数,检验上传文件的格式
// 文件列表发生变更时的回调函数,用于检验上传文件的格式
handleImportChange(file, fileList) {
const isXlsx = file.raw.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
const isXls = file.raw.type === 'application/vnd.ms-excel';
// 如果不是xlsx文件,也不是xls文件,则禁止放入文件列表。并进行友好提示
if(!(isXlsx || isXls)) {
this.$message({
type: 'error',
message: '上传的文件只能是xls或xlsx结尾的Excel文件,请重试'
});
// 将已经放在文件列表中的该文件删除
this.importFileList = [];
}
}
(2.4) 编写上传的文件超出限制时的回调函数
// 当用户想要上传超出limit限制的文件数量时的回调函数
excelHandleExceed() {
this.$message({
type: 'error',
message: '一次只能上传一个Excel文件!'
});
}
(2.5) 编写文件手动提交的方法
// 进行excel的真正提交,会提交到el-upload组件的action中配置的地址
submitImportExcel() {
this.$refs.excelUpload.submit();
}
(2.6)编写文件上传成功的回调函数
// Excel文件上传成功时的回调函数
uploadExcelSuccess(response) {
// 默认的上传文件成功的提示为成功标识
let messageType = 'success';
// 如果响应不为200,代表出现了错误
if(response.code != 200) {
messageType = 'error';
}
// 文件导入的提示信息
this.$message({
type: messageType,
message: response.msg
});
// 清空文件列表
this.importFileList = [];
// 关闭dialog弹出框
this.dialogImportExcelVisible = false;
// 初始化数据
this.initData();
}
当出现LocalDate时的处理
定义一个转换器
/**
* 可以看到Converter<LocalDate> 一共有6个可以实现的方法,我们先全部一起实现,然后看一下
*/
public class LocalDateConverter implements Converter<LocalDate> {
/**
* 这个方法,看名字就知道,这是支持的Java类型
* @return
*/
@Override
public Class<?> supportJavaTypeKey() {
return LocalDate.class;
}
/**
* 这里指的是Excel表格中对应列的类型,一般我们日期,就写String
* 他说需要一个CellDataTypeEnum,我们看看这个长啥样
* 发现她就是一个枚举,里面定义了类型,我们就选String
* @return
*/
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
/**
*
* @param cellData 当前列的数据,也就是你每次拿到的那个Excel表格中的那个日期: 例如1888-12-11
* @param contentProperty 这个是当前的上下文属性,可以不用暂时不用关注
* @param globalConfiguration 全局配置,不关注
* @return 从返回值可以看到,需要一个LocalDate,那么这一定是从Excel表格中读取日期,
* 然后需要转换为LocalDate
* @throws Exception
*/
@Override
public LocalDate convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
// 所以我们需要在这里把Excel表格中的日期转换为LocalDate
// cellData.getStringValue() 可以拿到表格中的日期,转换为String
String excelPublishDate = cellData.getStringValue();
// 将结果转换
LocalDate publishDate = LocalDate.parse(excelPublishDate, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
// 返回结果
return publishDate;
}
/**
* 你看这个,方法名字是不是跟上面那个很像很像,并且我试了从这个方法中得不到想要的信息,
* 所以这个方法我就不重写了。convertToJavaData(ReadConverterContext<?> context)
* @return
* @throws Exception
*/
/**
*
* @param value 一看就是读取我们实体类的时候拿到的LocalDate
* @param contentProperty 不管
* @param globalConfiguration 不管
* @return 从方法形参中,我们看到,它将LocalDate value传递了给我们,其实这个方法呢
* 是将我们的LocalDate 写入到Excel当中,因此我们需要在这里把他转换成字符串
* ,而我们看到,她的返回值是一个WriteCellData,我们看一下它到底是个啥
* ,我们发现它就是一个类,而且有一个构造函数,刚好可以传递一个String,我其实并不知道它行不行
* 所以我试了试
* @throws Exception
*/
@Override
public WriteCellData<?> convertToExcelData(LocalDate value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
// 这样就将LocalDate 转换成了String,交给了WriteCellData对象
return new WriteCellData<>(value.toString());
}
/**
* convertToExcelData(WriteConverterContext<LocalDate> context)
* 看这个方法的形参,明显对我们也没啥用,直接不重写改方法了
* @param context
* @return
* @throws Exception
*/
}
在实体类中指定转换器
/**
* 首先,如果使用EasyExcel来进行文件的导入和导出,
* 相对应的实体类如果不是Date,而是LocalDateTime或者LocalDate等其他类型
* 1、如果不指定转化器,EasyExcel在进行写操作时,就会直接报错:找不到转换器异常
* 2、因此我们需要为他配置一个转换器,因此我去查阅了官方文档,它上面说写一个转换器需要
* 写一个类来实现Converter,注意这个Converter的包,一定要导入easyexcel中的。
*/
@ExcelProperty(value = "发布日期",converter = LocalDateConverter.class)
private LocalDate publishDate;
第五问:SpringBoot项目中,Jackson转换Long、LocalDate、LocalDateTime问题
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
@Configuration
public class JacksonConfig {
/**
* 转换器是为了解决表单提交时的序列化问题。 String转换为LocalDateTime
* @return
*/
@Bean
public Converter<String, LocalDateTime> localDateTimeConverter() {
return new Converter<String, LocalDateTime>() {
@Override
public LocalDateTime convert(String source) {
return LocalDateTime.parse(source, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
};
}
/**
* String转换为LocalDate
* @return
*/
@Bean
public Converter<String, LocalDate> localDateConverter() {
return new Converter<String, LocalDate>() {
@Override
public LocalDate convert(String source) {
return LocalDate.parse(source, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
}
};
}
/**
* String转换为LocalTime
* @return
*/
@Bean
public Converter<String, LocalTime> localTimeConverter() {
return new Converter<String, LocalTime>() {
@Override
public LocalTime convert(String source) {
return LocalTime.parse(source, DateTimeFormatter.ofPattern("HH:mm:ss"));
}
};
}
/**
* 自定义jackson行为,在进行解析时,将其转换为指定格式,这里面的序列化与反序列化指的是
* 1、接收json数据进行反序列化时
* 2、返回json数据进行序列化时
* @return
*/
@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
return builder -> {
final String STANDARD_PATTERN = "yyyy-MM-dd HH:mm:ss";
final String DATE_PATTERN = "yyyy-MM-dd";
final String TIME_PATTERN = "HH:mm:ss";
// 初始化JavaTimeModule
JavaTimeModule javaTimeModule = new JavaTimeModule();
//处理LocalDateTime
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(STANDARD_PATTERN);
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(dateTimeFormatter));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(dateTimeFormatter));
//处理LocalDate
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(DATE_PATTERN);
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(dateFormatter));
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(dateFormatter));
//处理LocalTime
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern(TIME_PATTERN);
javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(timeFormatter));
javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(timeFormatter));
builder.simpleDateFormat(STANDARD_PATTERN)
.modules(javaTimeModule, new Jdk8Module())
// 防止序列化Long给前端时,精度丢失问题
.serializationInclusion(JsonInclude.Include.NON_NULL)
.serializerByType(Long.class, ToStringSerializer.instance)
.serializerByType(Long.TYPE, ToStringSerializer.instance)
.failOnEmptyBeans(false)
.failOnUnknownProperties(false)
.featuresToEnable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN)
.featuresToEnable(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS);
};
}
}
第六问:web项目的统一响应结果
import org.springframework.http.HttpStatus;
import java.util.HashMap;
import java.util.Map;
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
public R() {
put("code", 200);
put("msg", "success");
}
public static R error() {
return error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "未知异常,请联系管理员");
}
public static R error(String msg) {
return error(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg);
}
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}
public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
@Override
public R put(String key, Object value) {
super.put(key, value);
return this;
}
}
第7问:使用阿里云实现OSS文件直传服务
在阿里云控制台中进行如下配置
开通阿里云的对象存储服务,并创建一个Bucket(设置公共读)

获取上传文件时的必备信息(EnterPoint、AccessKey、Access Secret)
EnterPoint访问端点

创建Access子用户并拿到其AccessKey、Access Secret(别忘了选中API调用访问)


设置Bucket的允许跨域,以及用户的访问权限


使用原始SDK完成文件的上传(服务器端直接传,使用文件流的方式)
-
具体查看阿里云官方文档
-
添加依赖
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>
- 编写上传代码
@Test
public void oss() throws FileNotFoundException {
String filePath = "C:\\Users\\DQX\\Desktop\\woniu\\crebas.sql";
// yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
String endpoint = "oss-cn-shenzhen.aliyuncs.com";
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessKeyId = "LTAI5tGPAQrP6hKHkfwYv6NT";
String accessKeySecret = "lvq9Nd00KS6Fc90Aq3Vh3ueZD5yeqh";
// 填写Bucket名称,例如examplebucket。
String bucketName = "gulimall-codestars";
String objectName = "exampledir/exampleobject.txt";
InputStream inputStream = new FileInputStream(filePath);
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, inputStream);
System.out.println("文件上传成功");
// 关闭OSSClient。
ossClient.shutdown();
}
配合SpringBoot实现服务端签名后直传
- 具体可查看阿里云官方文档
引入OSS依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>aliyun-oss-spring-boot-starter</artifactId>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>aliyun-spring-boot-dependencies</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
修改application.yml配置文件,配置上传文件必备的4要素
alibaba:
cloud:
access-key: LTAI5tGPAQrP6hKHkfwYv6NT
secret-key: lvq9Nd00KS6Fc90Aq3Vh3ueZD5yeqh
oss:
endpoint: oss-cn-shenzhen.aliyuncs.com
bucketName: gulimall-codestars
R统一响应工具类(查看第6问)
编写一个接口,让前端获取上传的签名信息
import com.aliyun.oss.OSS;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.MatchMode;
import com.aliyun.oss.model.PolicyConditions;
import com.codestars.common.utils.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author codeStars
* @date 2022/9/29 10:35
*/
@RestController
@RequestMapping("/oss")
public class OSSController {
@Autowired
private OSS ossClient;
@Value("${alibaba.cloud.access-key}")
private String accessId;
@Value("${alibaba.cloud.secret-key}")
private String accessKey;
@Value("${alibaba.cloud.oss.endpoint}")
private String endpoint;
@Value("${alibaba.cloud.oss.bucketName}")
private String bucketName;
/**
* @return 提供给前端签名后直传所需要的数据
*/
@RequestMapping("/policy")
public R getPolicy() {
// 填写Host地址,格式为https://bucketname.endpoint。
String host = String.format("https://%s.%s", bucketName, endpoint);
// 设置上传到OSS文件的前缀,可置空此项。置空后,文件将上传至Bucket的根目录下。
String dir = LocalDate.now().toString() + "/";
Map<String, String> respMap = null;
try {
long expireTime = 30;
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expiration = new Date(expireEndTime);
PolicyConditions policyConds = new PolicyConditions();
policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
byte[] binaryData = postPolicy.getBytes("utf-8");
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = ossClient.calculatePostSignature(postPolicy);
respMap = new LinkedHashMap<String, String>();
respMap.put("accessId", accessId);
respMap.put("policy", encodedPolicy);
respMap.put("signature", postSignature);
respMap.put("dir", dir);
respMap.put("host", host);
respMap.put("expire", String.valueOf(expireEndTime / 1000));
// respMap.put("expire", formatISO8601Date(expiration));
} catch (Exception e) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
}
return R.ok().put("data", respMap);
}
}
前端使用vue配合elementui完成文件的上传
1、编写文件上传的组件(注意action为https://BucketName.EnterPoint)
<el-upload
class="upload-demo"
action="http://gulimall-codestars.oss-cn-shenzhen.aliyuncs.com"
:data="dataObj"
:before-upload="beforeUpload"
:on-success="uploadSuccess"
:file-list="fileList"
:multiple="false"
:limit="1"
list-type="picture">
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
</el-upload>
2、编写文件上传时所需的data
data(){
return {
fileList: [],
dataObj: {
OSSAccessKeyId: '',
// 上传文件的名称,包括路径
key: '',
policy: '',
signature: '',
dir: '',
host: ''
}
}
3、编写文件上传之前的回调函数(获取需要携带的签名等数据)
如下的方法,都是放在methods中的
// 1、上传文件提交前,需要先配置请求时携带的数据
beforeUpload(file) {
let _self = this;
return new Promise((resolve, reject) => {
axios.get('/edusys/oss/policy')
.then(response => {
_self.dataObj.policy = response.data.data.policy;
_self.dataObj.signature = response.data.data.signature;
_self.dataObj.OSSAccessKeyId = response.data.data.accessId;
// 发送图片时候,图片的最终地址为: host + key(文件的名称,如果中间有/,则代表目录),
// 这里的key是通过后端给的dir目录后拼接的
// 只有文件名需要注意:${filename}
_self.dataObj.key = response.data.data.dir + this.getUUID() + "_${filename}";
_self.dataObj.dir = response.data.data.dir;
_self.dataObj.host = response.data.data.host;
resolve(true);
})
.catch(err => {
console.log("出错了...",err)
reject(false);
});
});
},
getUUID () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
return (c === 'x' ? (Math.random() * 16 | 0) : ('r&0x3' | '0x8')).toString(16)
})
}
4、文件上传成功后,获取到其地址
// 2、文件上传成功后,修改当前需要提交到后台的picture的数据为图片地址
uploadSuccess(response, file, fileList) {
// 这里的this.newsInfoForm.picture相当于拿到了文件的访问地址,一个完整的url
this.newsInfoForm.picture = this.dataObj.host + "/" + this.dataObj.key.replace("${filename}", file.name)
}
这里提供2个封装好的文件上传组件
- 获取签名数据的policy.js
export function policy() {
return new Promise((resolve, reject) => {
axios.get('/edusys/oss/policy').then(data => {
resolve(data)
}).catch(err => reject())
})
}
- 封装单文件上传组件(图片)
<template>
<div>
<el-upload
action="http://gulimall-codestars.oss-cn-shenzhen.aliyuncs.com"
:data="dataObj"
list-type="picture"
:multiple="false" :show-file-list="showFileList"
:file-list="fileList"
:before-upload="beforeUpload"
:on-remove="handleRemove"
:on-success="handleUploadSuccess"
:on-preview="handlePreview">
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过10MB</div>
</el-upload>
<el-dialog :visible.sync="dialogVisible">
<img width="100%" :src="fileList[0].url" alt="">
</el-dialog>
</div>
</template>
<script>
import {policy} from './policy'
import { getUUID } from '@/utils'
export default {
name: 'singleUpload',
props: {
value: String
},
computed: {
imageUrl() {
return this.value;
},
imageName() {
if (this.value != null && this.value !== '') {
return this.value.substr(this.value.lastIndexOf("/") + 1);
} else {
return null;
}
},
fileList() {
return [{
name: this.imageName,
url: this.imageUrl
}]
},
showFileList: {
get: function () {
return this.value !== null && this.value !== ''&& this.value!==undefined;
},
set: function (newValue) {
}
}
},
data() {
return {
dataObj: {
policy: '',
signature: '',
key: '',
ossaccessKeyId: '',
dir: '',
host: '',
// callback:'',
},
dialogVisible: false
};
},
methods: {
emitInput(val) {
this.$emit('input', val)
},
handleRemove(file, fileList) {
this.emitInput('');
},
handlePreview(file) {
this.dialogVisible = true;
},
beforeUpload(file) {
let _self = this;
return new Promise((resolve, reject) => {
policy().then(response => {
console.log("响应的数据",response);
_self.dataObj.policy = response.data.policy;
_self.dataObj.signature = response.data.signature;
_self.dataObj.ossaccessKeyId = response.data.accessId;
_self.dataObj.key = response.data.dir +getUUID()+'_${filename}';
_self.dataObj.dir = response.data.dir;
_self.dataObj.host = response.data.host;
console.log("响应的数据222。。。",_self.dataObj);
resolve(true)
}).catch(err => {
reject(false)
})
})
},
handleUploadSuccess(res, file) {
console.log("上传成功...")
this.showFileList = true;
this.fileList.pop();
this.fileList.push({name: file.name, url: this.dataObj.host + '/' + this.dataObj.key.replace("${filename}",file.name) });
this.emitInput(this.fileList[0].url);
}
}
}
</script>
<style>
</style>
- 封装多文件上传组件
<template>
<div>
<el-upload
action="http://gulimall-codestars.oss-cn-shenzhen.aliyuncs.com"
:data="dataObj"
:list-type="listType"
:file-list="fileList"
:before-upload="beforeUpload"
:on-remove="handleRemove"
:on-success="handleUploadSuccess"
:on-preview="handlePreview"
:limit="maxCount"
:on-exceed="handleExceed"
:show-file-list="showFile"
>
<i class="el-icon-plus"></i>
</el-upload>
<el-dialog :visible.sync="dialogVisible">
<img width="100%" :src="dialogImageUrl" alt />
</el-dialog>
</div>
</template>
<script>
import { policy } from "./policy";
import { getUUID } from '@/utils'
export default {
name: "multiUpload",
props: {
//图片属性数组
value: Array,
//最大上传图片数量
maxCount: {
type: Number,
default: 30
},
listType:{
type: String,
default: "picture-card"
},
showFile:{
type: Boolean,
default: true
}
},
data() {
return {
dataObj: {
policy: "",
signature: "",
key: "",
ossaccessKeyId: "",
dir: "",
host: "",
uuid: ""
},
dialogVisible: false,
dialogImageUrl: null
};
},
computed: {
fileList() {
let fileList = [];
for (let i = 0; i < this.value.length; i++) {
fileList.push({ url: this.value[i] });
}
return fileList;
}
},
mounted() {},
methods: {
emitInput(fileList) {
let value = [];
for (let i = 0; i < fileList.length; i++) {
value.push(fileList[i].url);
}
this.$emit("input", value);
},
handleRemove(file, fileList) {
this.emitInput(fileList);
},
handlePreview(file) {
this.dialogVisible = true;
this.dialogImageUrl = file.url;
},
beforeUpload(file) {
let _self = this;
return new Promise((resolve, reject) => {
policy()
.then(response => {
console.log("这是什么${filename}");
_self.dataObj.policy = response.data.policy;
_self.dataObj.signature = response.data.signature;
_self.dataObj.ossaccessKeyId = response.data.accessId;
_self.dataObj.key = response.data.dir +getUUID()+"_${filename}";
_self.dataObj.dir = response.data.dir;
_self.dataObj.host = response.data.host;
resolve(true);
})
.catch(err => {
console.log("出错了...",err)
reject(false);
});
});
},
handleUploadSuccess(res, file) {
this.fileList.push({
name: file.name,
// url: this.dataObj.host + "/" + this.dataObj.dir + "/" + file.name; 替换${filename}为真正的文件名
url: this.dataObj.host + "/" + this.dataObj.key.replace("${filename}",file.name)
});
this.emitInput(this.fileList);
},
handleExceed(files, fileList) {
this.$message({
message: "最多只能上传" + this.maxCount + "张图片",
type: "warning",
duration: 1000
});
}
}
};
</script>
<style>
</style>
- 使用方式
- 引入组件
import SingleUploadVue from '@/components/upload/singleUpload';
components: {
SingleUploadVue
}
- 页面中的使用
<SingleUploadVue v-model="dataForm.logo"></SingleUploadVue>
第8问:完成数据校验功能
前端使用Vue配合ElementUI完成数据校验功能
编写表单组件,并且在表单组件(绑定rules规则属性)当中,添加input组件(绑定prop属性)
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="活动名称" prop="name">
<el-input v-model="ruleForm.name"></el-input>
</el-form-item>
<el-form-item label="活动区域" prop="region">
<el-select v-model="ruleForm.region" placeholder="请选择活动区域">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
</el-form>
<el-button type="primary" @click="submitForm('ruleForm')">点击提交</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
编写规则(包含数据的绑定,校验器规则、以及校验事件、重置事件)
<script>
export default {
data() {
return {
ruleForm: {
name: '',
region: ''
},
rules: {
name: [
{ required: true, message: '请输入活动名称', trigger: 'blur' },
{ min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
],
region: [
{ required: true, message: '请选择活动区域', trigger: 'change' }
]
}
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
alert('submit!');
} else {
console.log('error submit!!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
}
}
</script>
自定义校验规则
- 使用方法
firstLetter: [
{ validator: this.validateFirstLetter , trigger: 'blur' }
]
- 校验器
validateFirstLetter(rule, value, callback){
const firstReg = /^[a-zA-Z]{1}$/
if (value === '' || !firstReg.test(value)) {
callback(new Error('检索首字母不能为空且只能由一个字母组成'));
} else {
callback();
}
}
后端采用JSR303进行校验
引入JSR303数据校验的依赖,SpringBoot还可以引入启动器
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
普通数据校验,不分组时(了解即可)
- 在控制器接口的某个需要校验的接收参数上,添加@Valid
public R save(@RequestBody @Valid BrandEntity brand, BindingResult result)
- 在需要校验的实体类中的字段,可以添加如下注解

@NotBlank
@URL(message = "logo必须为一个URL地址")
private String logo;
- 使用正则表达式
@NotBlank
@Pattern(regexp = "^[a-zA-Z]$")
private String firstLetter;
控制器上添加了BindingResult 与不加BindingResult的区别
- 如果加了BindingResult,就需要自行对异常进行处理
@RequestMapping("/save")
public R save(@RequestBody @Valid BrandEntity brand, BindingResult result){
if (result.getErrorCount() > 0) {
return R.error( 400, "参数异常");
}
brandService.save(brand);
return R.ok();
}
- 如果不加,则会抛出异常
@RequestMapping("/save")
public R save(@RequestBody @Valid BrandEntity brand){
brandService.save(brand);
return R.ok();
}

分组数据校验
创建几个接口,接口中什么东西都不用添加
public interface AddGroup {
}
public interface UpdateGroup {
}
为实体类中添加的校验注解添加上group属性(你可以不加,但是加了就一定要符合规则)
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@TableId
@JsonSerialize(using= ToStringSerializer.class)
@NotNull(groups = {UpdateGroup.class},message = "修改时,品牌ID不能为空")
@Null(groups = {AddGroup.class},message = "新增时品牌ID必须为空")
private Long brandId;
/**
* 品牌名
*/
@NotBlank(groups = {AddGroup.class, UpdateGroup.class}, message = "品牌名称不能为空")
private String name;
/**
* 品牌logo地址,这里的校验可以看出,在UpdateGroup分组中,可以不携带logo属性
* 但是一旦携带了,就必须为一个正确的URL地址
*/
@NotBlank(groups = AddGroup.class, message = "新增时,logo地址不能为空")
@URL(groups = {AddGroup.class, UpdateGroup.class}, message = "logo必须为一个URL地址")
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull(groups = AddGroup.class,message = "新增时,显示状态不能为空")
@StatusValid(groups = {AddGroup.class, UpdateGroup.class})
private Integer showStatus;
/**
* 检索首字母
*/
@Pattern(regexp = "^[a-zA-Z]$", groups = {AddGroup.class, UpdateGroup.class}, message = "检索首字母只能为单个字母")
private String firstLetter;
/**
* 排序
*/
@NotNull(groups = AddGroup.class, message="新增时排序不能为空")
private Integer sort;
}
替换控制器接口上的注解为@Validated
/**
* 保存
*/
@RequestMapping("/save")
public R save(@RequestBody @Validated(AddGroup.class) BrandEntity brand){
brandService.save(brand);
return R.ok();
}
/**
* 修改
*/
@RequestMapping("/update")
public R update(@RequestBody @Validated(UpdateGroup.class) BrandEntity brand){
brandService.updateById(brand);
return R.ok();
}
自定义校验注解
创建一个注解,指定校验器
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {StatusConstraintValidator.class})
public @interface StatusValid {
String message() default "{javax.validation.constraints.showstatus.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
创建一个校验器
public class StatusConstraintValidator implements ConstraintValidator<StatusValid, Integer> {
private Set<Integer> set = new HashSet<>();
/**
* 初始化方法
* @param constraintAnnotation
*/
@Override
public void initialize(StatusValid constraintAnnotation) {
set.add(1);
set.add(0);
}
/**
* 是否校验通过
* @param value
* @param context
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
在resources下创建一个ValidationMessages.properties文件(若出现乱码,修改idea的file encoding并重新创建该文件)
javax.validation.constraints.showstatus.message=状态信息只能为数字 1[显示]或0[不显示]
在需要只用自定义校验注解的字段上添加即可
@NotNull
@StatusValid
private Integer showStatus;
自定义全局异常统一处理(此时控制器上不能再写BindingResult)
定义一个枚举,用于统一状态码和错误信息
public enum BizCodeEnum {
UNKNOWN_EXCEPTION(10000, "系统未知异常"),
VALID_EXCEPTION(10001, "参数格式校验失败");
@Getter
private Integer code;
@Getter
private String message;
BizCodeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}
定义全局异常处理类
@RestControllerAdvice
@Slf4j
public class GulimallExceptionControllerAdvice {
/**
* 处理数据校验异常
* @param e
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public R validateExceptionHandler(MethodArgumentNotValidException e) {
log.error("数据校验出现问题{},异常类型{}", e.getMessage(), e.getClass());
// 获取到所有的错误字段信息
BindingResult bindingResult = e.getBindingResult();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
// 将其封装到一个Map集合当中
Map<String, String> errorMap = new HashMap<>();
fieldErrors.forEach(fieldError -> errorMap.put(fieldError.getField(), fieldError.getDefaultMessage()));
// 响应结果
return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(), BizCodeEnum.VALID_EXCEPTION.getMessage()).put("data", errorMap);
}
/**
* 兜底异常处理
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
public R validateExceptionHandler(Exception e) {
log.error("系统出现异常{},异常类型{}", e.getMessage(), e.getClass());
// 响应结果
return R.error(BizCodeEnum.UNKNOWN_EXCEPTION.getCode(), BizCodeEnum.UNKNOWN_EXCEPTION.getMessage());
}
}
第9问:使用MinIO完成签名后直传功能
搭建MinIO环境
使用docker安装MinIO
# 拉取镜像
docker pull minio/minio
# 启动容器
docker run -itd --net host \
--name minio9000 \
-e "MINIO_ACCESS_KEY=minioadmin" \
-e "MINIO_SECRET_KEY=minioadmin" \
-v /opt/docker/minio/data:/data \
-v /opt/docker/minio/config:/root/.minio \
minio/minio server /data --console-address ":19000" -address ":9000"
通过控制台访问MinIO的控制台并创建桶子(注意桶子需要设置为公共读)



SpringBoot集成MinIO(这里只是放一下minio7.0.2版本的使用api)
引入MinIO的依赖
<!--minioclient-->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>7.0.2</version>
</dependency>
在yml中添加自定义配置项
# 自定义配置项,方便在代码中使用
minio:
endpoint: 192.168.75.128
port: 9000
accessKey: minioadmin
secretKey: minioadmin
bucketName: sdd
编写minio配置类,获取到application.yml中的配置
import io.minio.MinioClient;
import io.minio.errors.InvalidEndpointException;
import io.minio.errors.InvalidPortException;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
/*加载yml文件中以minio开头的配置项*/
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {
/*会自动的对应配置项中对应的key*/
private String endpoint;//minio.endpoint
private String accessKey;
private String secretKey;
private Integer port;
/*把官方提供的MinioClient客户端注册到IOC容器中*/
@Bean
public MinioClient getMinioClient() throws InvalidEndpointException, InvalidPortException {
MinioClient minioClient = new MinioClient(getEndpoint(), getPort(), getAccessKey(), getSecretKey(), false);
return minioClient;
}
}
编写一个工具类,用于文件上传与下载
import io.minio.ObjectStat;
import io.minio.PutObjectOptions;
import io.minio.Result;
import io.minio.errors.*;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteError;
import io.minio.messages.Item;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
@Component
public class MinioClientUtil {
@Value("${minio.bucketName}")
private String bucketName;
@Autowired
private io.minio.MinioClient minioClient;
private static final int DEFAULT_EXPIRY_TIME = 7 * 24 * 3600;
/**
* 检查存储桶是否存在
*/
public boolean bucketExists(String bucketName) throws InvalidKeyException, ErrorResponseException,
IllegalArgumentException, InsufficientDataException, InternalException, InvalidBucketNameException,
InvalidResponseException, NoSuchAlgorithmException, XmlParserException, IOException {
boolean flag = minioClient.bucketExists(this.bucketName);
if (flag) return true;
return false;
}
/**
* 创建存储桶
*/
public boolean makeBucket() throws Exception {
boolean flag = bucketExists(bucketName);
if (!flag) {
minioClient.makeBucket(bucketName);
return true;
} else {
return false;
}
}
/**
* 列出所有存储桶名称
*/
public List<String> listBucketNames() throws Exception {
List<Bucket> bucketList = listBuckets();
List<String> bucketListName = new ArrayList<>();
for (Bucket bucket : bucketList) {
bucketListName.add(bucket.name());
}
return bucketListName;
}
/**
* 列出所有存储桶
*/
public List<Bucket> listBuckets() throws Exception {
return minioClient.listBuckets();
}
/**
* 删除存储桶
*
*/
public boolean removeBucket() throws Exception {
boolean flag = bucketExists(bucketName);
if (flag) {
Iterable<Result<Item>> myObjects = listObjects(bucketName);
for (Result<Item> result : myObjects) {
Item item = result.get();
// 有对象文件,则删除失败
if (item.size() > 0) {
return false;
}
}
// 删除存储桶,注意,只有存储桶为空时才能删除成功。
minioClient.removeBucket(bucketName);
flag = bucketExists(bucketName);
if (!flag) {
return true;
}
}
return false;
}
/**
* 列出存储桶中的所有对象名称
*
*/
public List<String> listObjectNames() throws Exception {
List<String> listObjectNames = new ArrayList<>();
boolean flag = bucketExists(bucketName);
if (flag) {
Iterable<Result<Item>> myObjects = listObjects(bucketName);
for (Result<Item> result : myObjects) {
Item item = result.get();
listObjectNames.add(item.objectName());
}
}
return listObjectNames;
}
/**
* 列出存储桶中的所有对象
*
* @param bucketName 存储桶名称
*/
public Iterable<Result<Item>> listObjects(String bucketName) throws Exception {
boolean flag = bucketExists(this.bucketName);
if (flag) {
return minioClient.listObjects(this.bucketName);
}
return null;
}
/**
* 把文件上传到minio
*
* @param objectName 存储桶里的对象名称
* @param fileName File name
*/
public boolean putObject(String objectName, String fileName) throws Exception {
boolean flag = bucketExists(bucketName);
if (flag) {
minioClient.putObject(bucketName, objectName, fileName, null);
ObjectStat statObject = statObject(objectName);
if (statObject != null && statObject.length() > 0) {
return true;
}
}
return false;
}
/**
* 通过InputStream上传对象
*
* @param objectName 存储桶里的对象名称
* @param stream 要上传的流
*/
public boolean putObject(String objectName, InputStream stream) throws Exception {
boolean flag = bucketExists(bucketName);
if (flag) {
minioClient.putObject(bucketName, objectName, stream, new PutObjectOptions(stream.available(), -1));
ObjectStat statObject = statObject(objectName);
if (statObject != null && statObject.length() > 0) {
return true;
}
}
return false;
}
/**
* 以流的形式获取一个文件对象
*
* @param objectName 存储桶里的对象名称
*/
public InputStream getObject( String objectName)throws Exception {
boolean flag = bucketExists(bucketName);
if (flag) {
ObjectStat statObject = statObject(objectName);
if (statObject != null && statObject.length() > 0) {
InputStream stream = minioClient.getObject(bucketName, objectName);
return stream;
}
}
return null;
}
/**
* 以流的形式获取一个文件对象(断点下载)
*
* @param objectName 存储桶里的对象名称
* @param offset 起始字节的位置
* @param length 要读取的长度 (可选,如果无值则代表读到文件结尾)
*/
public InputStream getObject(String objectName, long offset, Long length) throws Exception {
boolean flag = bucketExists(bucketName);
if (flag) {
ObjectStat statObject = statObject(objectName);
if (statObject != null && statObject.length() > 0) {
InputStream stream = minioClient.getObject(bucketName, objectName, offset, length);
return stream;
}
}
return null;
}
/**
* 下载并将文件保存到本地
*
* @param objectName 存储桶里的对象名称
* @param fileName File name
*/
public boolean getObject(String objectName, String fileName)throws Exception {
boolean flag = bucketExists(bucketName);
if (flag) {
ObjectStat statObject = statObject(objectName);
if (statObject != null && statObject.length() > 0) {
minioClient.getObject(bucketName, objectName, fileName);
return true;
}
}
return false;
}
/**
* 删除一个对象
*
* @param objectName 存储桶里的对象名称
*/
public boolean removeObject(String objectName)throws Exception {
boolean flag = bucketExists(bucketName);
if (flag) {
minioClient.removeObject(bucketName, objectName);
return true;
}
return false;
}
/**
* 删除指定桶的多个文件对象,返回删除错误的对象列表,全部删除成功,返回空列表
*
* @param objectNames 含有要删除的多个object名称的迭代器对象
*/
public List<String> removeObject(List<String> objectNames) throws Exception {
List<String> deleteErrorNames = new ArrayList<>();
boolean flag = bucketExists(bucketName);
if (flag) {
Iterable<Result<DeleteError>> results = minioClient.removeObjects(bucketName, objectNames);
for (Result<DeleteError> result : results) {
DeleteError error = result.get();
deleteErrorNames.add(error.objectName());
}
}
return deleteErrorNames;
}
/**
* 生成一个给HTTP GET请求用的presigned URL。
* 浏览器/移动端的客户端可以用这个URL进行下载,即使其所在的存储桶是私有的。这个presigned URL可以设置一个失效时间,默认值是7天。
*
* @param objectName 存储桶里的对象名称
* @param expires 失效时间(以秒为单位),默认是7天,不得大于七天
*/
public String presignedGetObject(String objectName, Integer expires) throws Exception {
boolean flag = bucketExists(bucketName);
String url = "";
if (flag) {
if (expires < 1 || expires > DEFAULT_EXPIRY_TIME) {
throw new InvalidExpiresRangeException(expires,
"expires must be in range of 1 to " + DEFAULT_EXPIRY_TIME);
}
url = minioClient.presignedGetObject(bucketName, objectName, expires);
}
return url;
}
/**
* 生成一个给HTTP PUT请求用的presigned URL。
* 浏览器/移动端的客户端可以用这个URL进行上传,即使其所在的存储桶是私有的。这个presigned URL可以设置一个失效时间,默认值是7天。
*
* @param objectName 存储桶里的对象名称
* @param expires 失效时间(以秒为单位),默认是7天,不得大于七天
*/
public String presignedPutObject(String objectName, Integer expires) throws Exception {
boolean flag = bucketExists(bucketName);
String url = "";
if (flag) {
if (expires < 1 || expires > DEFAULT_EXPIRY_TIME) {
throw new InvalidExpiresRangeException(expires,
"expires must be in range of 1 to " + DEFAULT_EXPIRY_TIME);
}
url = minioClient.presignedPutObject(bucketName, objectName, expires);
}
return url;
}
/**
* 获取对象的元数据
*
* @param objectName 存储桶里的对象名称
*/
public ObjectStat statObject(String objectName) throws Exception {
boolean flag = bucketExists(bucketName);
if (flag) {
ObjectStat statObject = minioClient.statObject(bucketName, objectName);
return statObject;
}
return null;
}
/**
* 文件访问路径
*
* @param objectName 存储桶里的对象名称
*/
public String getObjectUrl(String objectName) throws Exception {
boolean flag = bucketExists(bucketName);
String url = "";
if (flag) {
url = minioClient.getObjectUrl(bucketName, objectName);
}
return url;
}
}
控制器代码
@RequestMapping("/file")
@RestController
public class FileUpdateController {
@Resource
private MinioClient minioClient;
@PostMapping("/upload")
@ResponseBody
public CodeResult minioUpdate(@RequestPart MultipartFile imgFile) throws Exception {
/*上传成功返回图片的路径*/
String newFileUrl="";
/*获取原始文件名称*/
String oldFileName=imgFile.getOriginalFilename();
/*使用UUID生成新的文件名称*/
String newFileName=UUID.randomUUID().toString()+oldFileName.substring(oldFileName.lastIndexOf("."));
boolean flag=minioClient.putObject(newFileName,imgFile.getInputStream());
if(flag){
return CodeResult.ok().data("newName",newFileName);
}
return CodeResult.fail();
}
}
重要:SpringBoot集成Minio完成签名后直传功能
后端实现
引入依赖
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.4.5</version>
</dependency>
<dependency>
<groupId>me.tongfei</groupId>
<artifactId>progressbar</artifactId>
<version>0.5.3</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.8.1</version>
</dependency>
修改配置文件application.yml
minio:
endpoint: 81.68.124.11
port: 9000
accessKey: minioadmin
secretKey: minioadmin
bucketName: sddonline
# 文件路径分隔符
separator: /
编写一个Properties属性类来注入这些配置,并且注入一个MinioClient到容器中
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {
private String endpoint;
private String accessKey;
private String secretKey;
private Integer port;
@Bean
public MinioClient getMinioClient(){
return MinioClient.builder()
// 设置访问的地址和端口号,不开启TLS安全
.endpoint(this.endpoint, this.port, false)
// 设置账号和密码
.credentials(this.accessKey, this.secretKey).build();
}
}
minio中8.4.5版本的API工具类,注意其中的一个核心方法,获取签名信息
import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.*;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
@Component
public class MinioClientUtil {
@Value("${minio.bucketName}")
private String bucketName;
@Value("${minio.separator}")
private String separator;
@Autowired
private MinioClient minioClient;
// 设置最大超时时间
private static final int DEFAULT_EXPIRY_TIME = 7 * 24 * 3600;
/**
* 检查存储桶是否存在
*/
public boolean bucketExists() throws IOException, InvalidKeyException, InvalidResponseException, InsufficientDataException, NoSuchAlgorithmException, ServerException, InternalException, XmlParserException, ErrorResponseException {
return minioClient.bucketExists(BucketExistsArgs.builder().bucket(this.bucketName).build());
}
/**
* 签名后直传,获取签名
*
* 生成一个给HTTP PUT请求用的presigned URL。
* 浏览器/移动端的客户端可以用这个URL进行上传,即使其所在的存储桶是私有的。这个presigned URL可以设置一个失效时间
* 注意前端需要通过post请求来访问,请求路径为: http://endpoint:端口/桶名,
* 例如: http://localhost:9000/myBucket
* 前端数据通过formData的方式传递,一定需要携带一个key,该key就是存储文件的路径
*
* @param fileName 文件名称,主要用于获取到后缀
* @param minutesExpires 失效时间(以分钟为单位)
*/
public Map<String, String> presignedPutObject(String fileName, Integer minutesExpires) throws Exception {
boolean flag = bucketExists();
Map<String, String> policyMap = null;
if (flag) {
if (minutesExpires < 1 || minutesExpires > DEFAULT_EXPIRY_TIME) {
throw new RuntimeException(minutesExpires + "expires must be in range of 1 to" + DEFAULT_EXPIRY_TIME);
}
// 如下的这几行代码需要重点注意!!!!!!!!!
// 拼接文件路径,示例: 2022-10-27/60df4c72-8f0a-41dc-ae89-70af8d97eeef.jpg
String filePath = LocalDate.now().toString() + this.separator + UUID.randomUUID().toString() + fileName.substring(fileName.lastIndexOf("."));
// 获取前端post直传的策略,指定 桶名称 和 超时时间
PostPolicy policy = new PostPolicy(this.bucketName, ZonedDateTime.now().plusMinutes(minutesExpires));
// 添加策略的必备参数,key为最终文件保存的路径,路径就是filePath了,其中的 / 会被当成目录,
// 存储后就是在2022-10-27这个文件夹下有一个文件,文件名为:60df4c72-8f0a-41dc-ae89-70af8d97eeef.jpg
policy.addEqualsCondition("key", filePath);
// 获取到前端请求时需要携带的数据
policyMap = minioClient.getPresignedPostFormData(policy);
// 返回存储的路径
policyMap.put("key", filePath);
}
return policyMap;
}
/**
* 创建存储桶
*/
public boolean makeBucket() throws Exception {
boolean flag = bucketExists();
if (!flag) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(this.bucketName).build());
return true;
} else {
return false;
}
}
/**
* 列出所有存储桶名称
*/
public List<String> listBucketNames() throws Exception {
List<Bucket> bucketList = listBuckets();
List<String> bucketListName = new ArrayList<>();
for (Bucket bucket : bucketList) {
bucketListName.add(bucket.name());
}
return bucketListName;
}
/**
* 列出所有存储桶
*/
public List<Bucket> listBuckets() throws Exception {
return minioClient.listBuckets();
}
/**
* 删除存储桶
*/
public boolean removeBucket() throws Exception {
boolean flag = bucketExists();
if (flag) {
Iterable<Result<Item>> myObjects = listObjects(bucketName);
for (Result<Item> result : myObjects) {
Item item = result.get();
// 有对象文件,则删除失败
if (item.size() > 0) {
return false;
}
}
// 删除存储桶,注意,只有存储桶为空时才能删除成功。
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(this.bucketName).build());
flag = bucketExists();
if (!flag) {
return true;
}
}
return false;
}
/**
* 列出存储桶中的所有对象名称
*/
public List<String> listObjectNames() throws Exception {
List<String> listObjectNames = new ArrayList<>();
boolean flag = bucketExists();
if (flag) {
Iterable<Result<Item>> myObjects = listObjects(bucketName);
for (Result<Item> result : myObjects) {
Item item = result.get();
listObjectNames.add(item.objectName());
}
}
return listObjectNames;
}
/**
* 列出存储桶中的所有对象
*
* @param bucketName 存储桶名称
*/
public Iterable<Result<Item>> listObjects(String bucketName) throws Exception {
boolean flag = bucketExists();
if (flag) {
return minioClient.listObjects(ListObjectsArgs.builder().bucket(this.bucketName).build());
}
return null;
}
/**
* 把文件上传到minio
*
* @param objectName 存储桶里的对象名称
* @param fileName File name
*/
public boolean putObject(String objectName, String fileName) throws Exception {
boolean flag = bucketExists();
if (flag) {
FileInputStream inputStream = new FileInputStream(new File(fileName));
minioClient.putObject(PutObjectArgs.builder()
.bucket(this.bucketName)
.object(objectName)
.stream(inputStream, inputStream.available(), -1)
.build());
StatObjectResponse statObject = statObject(objectName);
if (statObject != null && statObject.size() > 0) {
return true;
}
}
return false;
}
/**
* 通过InputStream上传对象
*
* @param objectName 存储桶里的对象名称
* @param stream 要上传的流
*/
public boolean putObject(String objectName, InputStream stream) throws Exception {
boolean flag = bucketExists();
if (flag) {
minioClient.putObject(PutObjectArgs.builder()
.bucket(this.bucketName)
.object(objectName)
.stream(stream, stream.available(), -1)
.build());
StatObjectResponse statObject = statObject(objectName);
if (statObject != null && statObject.size() > 0) {
return true;
}
}
return false;
}
/**
* 以流的形式获取一个文件对象
*
* @param objectName 存储桶里的对象名称
*/
public InputStream getObject(String objectName) throws Exception {
boolean flag = bucketExists();
if (flag) {
StatObjectResponse statObject = statObject(objectName);
if (statObject != null && statObject.size() > 0) {
InputStream stream = minioClient.getObject(GetObjectArgs.builder().bucket(this.bucketName).object(objectName).build());
return stream;
}
}
return null;
}
/**
* 以流的形式获取一个文件对象(断点下载)
*
* @param objectName 存储桶里的对象名称
* @param offset 起始字节的位置
* @param length 要读取的长度 (可选,如果无值则代表读到文件结尾)
*/
public InputStream getObject(String objectName, long offset, Long length) throws Exception {
boolean flag = bucketExists();
if (flag) {
StatObjectResponse statObject = statObject(objectName);
if (statObject != null && statObject.size() > 0) {
InputStream stream = minioClient.getObject(
GetObjectArgs.builder()
.bucket(this.bucketName)
.object(objectName)
.offset(offset).length(length).build());
return stream;
}
}
return null;
}
/**
* 下载并将文件保存到本地
*
* @param objectName 存储桶里的对象名称
* @param fileName File name
*/
public boolean getObject(String objectName, String fileName) throws Exception {
boolean flag = bucketExists();
if (flag) {
StatObjectResponse statObject = statObject(objectName);
if (statObject != null && statObject.size() > 0) {
GetObjectResponse in = minioClient.getObject(GetObjectArgs.builder().bucket(this.bucketName).object(objectName).build());
IOUtils.copy(in, new FileOutputStream(fileName));
return true;
}
}
return false;
}
/**
* 删除一个对象
*
* @param objectName 存储桶里的对象名称
*/
public boolean removeObject(String objectName) throws Exception {
boolean flag = bucketExists();
if (flag) {
minioClient.removeObject(RemoveObjectArgs.builder().bucket(this.bucketName).object(objectName).build());
return true;
}
return false;
}
/**
* 删除指定桶的多个文件对象,返回删除错误的对象列表,全部删除成功,返回空列表
*
* @param objectNames 含有要删除的多个object名称的迭代器对象
*/
public List<String> removeObject(List<String> objectNames) throws Exception {
List<String> deleteErrorNames = new ArrayList<>();
boolean flag = bucketExists();
if (flag) {
// 将objectNames转换成DeleteObject再转换为List,List间接是继承了Iterable的
// 最终获取到了Iterable<Result<DeleteError>>结果集
Iterable<Result<DeleteError>> results = minioClient.removeObjects(
RemoveObjectsArgs.builder()
.bucket(this.bucketName)
.objects(objectNames.stream().map(objName -> new DeleteObject(objName)).collect(Collectors.toList()))
.build());
// 看看是不是有错误,有错误的化将错误信息添加到deleteErrorNames 这个List集合当中
for (Result<DeleteError> result : results) {
DeleteError error = result.get();
deleteErrorNames.add(error.objectName());
}
}
return deleteErrorNames;
}
/**
*
* 生成一个给HTTP GET请求用的presigned URL。相当于一个临时URL
*
* 浏览器/移动端的客户端可以用这个URL进行下载,即使其所在的存储桶是私有的。这个presigned URL可以设置一个失效时间,默认值是7天。
*
* @param objectName 存储桶里的对象名称
* @param expires 失效时间(以秒为单位),默认是7天,不得大于七天
*/
public String presignedGetObject(String objectName, Integer expires) throws Exception {
boolean flag = bucketExists();
String url = "";
if (flag) {
if (expires < 1 || expires > DEFAULT_EXPIRY_TIME) {
throw new RuntimeException(expires + "expires must be in range of 1 to" + DEFAULT_EXPIRY_TIME);
}
url = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.bucket(this.bucketName)
.method(Method.GET)
.object(objectName)
.expiry(expires).build());
}
return url;
}
/**
* 获取对象的元数据
*
* @param objectName 存储桶里的对象名称
*/
public StatObjectResponse statObject(String objectName) throws Exception {
boolean flag = bucketExists();
if (flag) {
StatObjectResponse statObjectResponse = minioClient.statObject(StatObjectArgs.builder().bucket(this.bucketName).object(objectName).build());
return statObjectResponse;
}
return null;
}
}
编写一个接口,用于给前端响应签名信息
import com.woniu.utils.MinioClientUtil;
import com.woniu.utils.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author codeStars
* @date 2022/10/27 9:51
*/
@RestController
@RequestMapping("/oss")
@Api("OSS对象存储服务接口")
public class OSSController {
@Autowired
private MinioClientUtil minioClientUtil;
@GetMapping("/policy")
@ApiOperation("获取上传文件所需的签名信息")
@ApiImplicitParam(name = "fileName", value = "获取用户上传文件的文件名,主要是为了得到它的后缀而已")
public R policy(String fileName) throws Exception {
return R.ok().put("data", minioClientUtil.presignedPutObject(fileName, 5));
}
}
前端实现(前端需要通过formData的方式传递数据,并且文件input的name属性必须为file)
编写一个policy.js 专门用于获取签名数据
- 注意一下,这里的这个request,仅仅是单纯封装了一下axios,设置了一个基础路径
import request from '@/utils/request'
export default function(fileName) {
return request({
url: '/sddonline/oss/policy',
method: 'get',
params: {
fileName
}
})
}
编写upload组件,核心是需要传递的data
-
需要注意其中的dataObj,需要绑定数据
-
注意before-upload,用于获取需要携带的额外数据
-
on-success 用户头像回显
-
action 文件上传的地址,为http://endpoint:port/bucketName
<el-upload
class="avatar-uploader"
action="http://81.68.124.11:9000/sddonline"
:show-file-list="false"
:data="dataObj"
name="file"
:on-success="uploadSuccess"
:before-upload="beforeUpload">
<!-- 用于头像的回显 -->
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
编写 上传之前(注意切换请求为同步) 和 上传之后 的方法
-
如果获取签名为异步调用,那么将会导致数据还没获取到,就已经提交了
-
此时就会出现如下异常
Error parsing the X-Amz-Credential parameter; the Credential is mal-formed; expecting \"<YOUR-AKID>/YYYYMMDD/REGION/SERVICE/aws4_request\".
- 实际代码
data() {
return {
dataObj: {},
imageUrl: null
};
},
methods: {
async beforeUpload(file) {
const response = await policy(file.name)
const data = response.data;
this.dataObj = {...data}
},
uploadSuccess(response, file){
this.imageUrl = this.$store.state.app.ossBasePath + "/" + this.dataObj.key
}
}
文件上传成功后将会得到如下响应

第10问:使用element-ui、EasyExcel、mybatisplus完成Excel的导入导出
后台处理
封装一个工具类EasyExcelUtil,实现一行代码导入导出
/**
* @author codeStars
* @date 2022/10/28 9:57
*/
public class EasyExcelUtil {
/**
* 需要导出的
* @param response 浏览器响应对象
* @param clazz 需要写到Excel中的类
* @param list 需要写入的数据
*/
public static void exportExcel(HttpServletResponse response, Class clazz, List list){
try {
// 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode(UUID.randomUUID().toString(), "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), clazz).sheet("sheet1").doWrite(list);
} catch (IOException e) {
throw new RuntimeException("Excel文件导出失败");
}
}
/**
* 抽取导入功能
* @param inputStream 需要读取的文件输入流
* @param clazz 读取的实体类型
* @param readListener 读取文件时的监听器
*/
public static void importExcel(InputStream inputStream, Class clazz, ReadListener<?> readListener) {
try {
EasyExcel.read(inputStream, clazz, readListener).sheet().doRead();
} catch (Exception e) {
throw new RuntimeException("Excel文件导入失败");
}
}
public static class DataListener<T> implements ReadListener<T> {
/**
* 用于存储每一条数据
*/
private List<T> cachedDataList = new ArrayList<>();
private int count = 0;
/**
* 访问数据库的业务对象
*/
private IService<T> service;
public DataListener(IService<T> service) {
this.service = service;
}
@Override
public void invoke(T t, AnalysisContext analysisContext) {
// 如果大于100了,则写入一次
if(this.count >= 100) {
service.saveBatch(this.cachedDataList);
this.cachedDataList.clear();
}
// 放入缓存,读取完毕后再一起写入
cachedDataList.add(t);
this.count ++;
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
// 最后再写入一次数据
service.saveBatch(this.cachedDataList);
}
}
}
编写控制器来使用该工具类
/**
* 上传个文件即可
* @param file
* @return
* @throws IOException
*/
@PostMapping("/importExcel")
public R importExcel(MultipartFile file) throws IOException {
EasyExcelUtil.importExcel(file.getInputStream(), Trainer.class, new EasyExcelUtil.DataListener<Trainer>(trainerService));
return R.ok("Excel数据导入成功");
}
/**
* 前端使用post请求,发送一个json对象,该json对象必须为一个数组,存放需要导出数据的id
* @param ids
* @param response
* @throws IOException
*/
@PostMapping("/exportExcel")
public void exportExcel(@RequestBody List<Long> ids, HttpServletResponse response) throws IOException {
// trainerService.getExportExcelList(ids); 该方法,如果传入的list长度为0,则会查询所有数据
List<Trainer> list = trainerService.getExportExcelList(ids);
EasyExcelUtil.exportExcel(response, Trainer.class, list);
}
编写LocalDateTime转换器
/**
* 可以看到Converter<LocalDateTime> 一共有6个可以实现的方法,我们先全部一起实现,然后看一下
*/
public class LocalDateTimeConverter implements Converter<LocalDateTime> {
/**
* 这个方法,看名字就知道,这是支持的Java类型
* @return
*/
@Override
public Class<?> supportJavaTypeKey() {
return LocalDateTime.class;
}
/**
* 这里指的是Excel表格中对应列的类型,一般我们日期,就写String
* 他说需要一个CellDataTypeEnum,我们看看这个长啥样
* 发现她就是一个枚举,里面定义了类型,我们就选String
* @return
*/
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
/**
*
* @param cellData 当前列的数据,也就是你每次拿到的那个Excel表格中的那个日期: 例如1888-12-11
* @param contentProperty 这个是当前的上下文属性,可以不用暂时不用关注
* @param globalConfiguration 全局配置,不关注
* @return 从返回值可以看到,需要一个LocalDateTime,那么这一定是从Excel表格中读取日期,
* 然后需要转换为LocalDateTime
* @throws Exception
*/
@Override
public LocalDateTime convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
// 所以我们需要在这里把Excel表格中的日期转换为LocalDateTime
// cellData.getStringValue() 可以拿到表格中的日期,转换为String
String excelPublishDate = cellData.getStringValue();
// 将结果转换
LocalDateTime publishDate = LocalDateTime.parse(excelPublishDate, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// 返回结果
return publishDate;
}
/**
*
* @param value 一看就是读取我们实体类的时候拿到的LocalDateTime
* @param contentProperty 不管
* @param globalConfiguration 不管
* @return 从方法形参中,我们看到,它将LocalDateTime value传递了给我们,其实这个方法呢
* 是将我们的LocalDateTime 写入到Excel当中,因此我们需要在这里把他转换成字符串
* ,而我们看到,她的返回值是一个WriteCellData,我们看一下它到底是个啥
* ,我们发现它就是一个类,而且有一个构造函数,刚好可以传递一个String,我其实并不知道它行不行
* 所以我试了试
* @throws Exception
*/
@Override
public WriteCellData<?> convertToExcelData(LocalDateTime value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
// 这样就将LocalDateTime 转换成了String,交给了WriteCellData对象
return new WriteCellData<>(value.toString());
}
}
在需要导入导出的实体类中添加注解等信息
@Getter
@Setter
@TableName("sdd_trainer")
@ApiModel(value = "Trainer对象", description = "培训师")
public class Trainer implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("培训师ID")
@ExcelIgnore
private Long id;
@ApiModelProperty("培训师姓名")
@ExcelProperty("培训师姓名")
private String name;
@ApiModelProperty("培训师简介")
@ExcelProperty("培训师简介")
private String intro;
@ApiModelProperty("培训师资历,一句话说明培训师")
@ExcelProperty("培训师资历")
private String career;
@ApiModelProperty("头衔 1高级培训师 2首席培训师")
@ExcelProperty("培训师级别,1高级培训师 2首席培训师")
private Integer level;
@ApiModelProperty("培训师头像")
@ExcelProperty("头像地址")
private String avatar;
@ApiModelProperty("排序")
@ExcelProperty("排序字段")
private Integer sort;
@ApiModelProperty("逻辑删除 1(true)已删除, 0(false)未删除")
@TableLogic
@ExcelProperty("是否删除,0未删除,1删除")
private Boolean isDeleted;
@ApiModelProperty("创建时间")
@TableField(fill = FieldFill.INSERT)
@ExcelProperty("创建时间")
private LocalDateTime createTime;
@ApiModelProperty("更新时间")
@TableField(fill = FieldFill.INSERT_UPDATE)
@ExcelProperty("更新时间")
private LocalDateTime updateTime;
}
前端实现
封装Excel的导入组件(注意组件的名称一定要带双引号)
<template>
<div id="content">
<el-button type="primary" icon="el-icon-search" @click="dialogImportExcelVisible = true">导入excel</el-button>
<!-- 上传excel导入功能 -->
<el-dialog
:close-on-click-modal="false"
title="导入excel"
:visible.sync="dialogImportExcelVisible">
<!--
ref: 该文件上传的组件指定一个ref,这样的话就可以通过 this.$refs.excelUpload的方式获取到该组件,才能调用该组件的方法,例如submit()
class: 这就不说了
action: 文件需要上传到的地址,填写刚刚控制器的地址,为什么要带/edusys上下文呢?因为浏览器识别/的时候,只会识别到http://localhost:80
:on-success: 当文件上传成功时的回调函数
:multiple="false" 禁止选中多个文件
:limit 设置文件的上传数量
:auto-upload="false" 关闭文件上传的自动提交功能
:file-list="importFileList" 会显示在页面中的文件列表
:on-change 当文件列表发生改变时,就会调用该方法
:on-exceed 当文件上传超出limit限制时,就会调用该方法,我这里的limit是1,因此只要上传第二个文件,那么就会调用该方法
-->
<el-upload
ref="excelUpload"
class="upload-excel"
:action="url"
:on-success="uploadExcelSuccess"
:multiple="false"
:limit="1"
:auto-upload="false"
:file-list="importFileList"
:on-change="handleImportChange"
:on-exceed="excelHandleExceed">
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传xlsx、xls结尾的Excel文件,并且一次只能上传一个!</div>
</el-upload>
<div slot="footer" class="dialog-footer">
<!-- 点击取消时,将该上传的dialog关闭 -->
<el-button @click="dialogImportExcelVisible = false">取 消</el-button>
<!-- 手动上传文件 -->
<el-button type="primary" @click="submitImportExcel">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
name: "ImportExcel",
props: ['url'],
data() {
return {
// 导入时的上传文件列表
importFileList: [],
// 导入excel的弹出框标识
dialogImportExcelVisible: false
}
},
methods: {
// 文件列表发生变更时的回调函数,用于检验上传文件的格式
handleImportChange(file, fileList) {
const isXlsx = file.raw.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
const isXls = file.raw.type === 'application/vnd.ms-excel';
// 如果不是xlsx文件,也不是xls文件,则禁止放入文件列表。并进行友好提示
if(!(isXlsx || isXls)) {
this.$message({
type: 'error',
message: '上传的文件只能是xls或xlsx结尾的Excel文件,请重试'
});
// 将已经放在文件列表中的该文件删除
this.importFileList = [];
}
},
// 当用户想要上传超出limit限制的文件数量时的回调函数
excelHandleExceed() {
this.$message({
type: 'error',
message: '一次只能上传一个Excel文件!'
});
},
// 进行excel的真正提交,会提交到el-upload组件的action中配置的地址
submitImportExcel() {
this.$refs.excelUpload.submit();
},
// Excel文件上传成功时的回调函数
uploadExcelSuccess(response) {
// 默认的上传文件成功的提示为成功标识
let messageType = 'success';
// 如果响应不为200,代表出现了错误
if(response.code != 200) {
messageType = 'error';
}
// 文件导入的提示信息
this.$message({
type: messageType,
message: response.msg
});
// 清空文件列表
this.importFileList = [];
// 关闭dialog弹出框
this.dialogImportExcelVisible = false;
// 初始化数据
this.$emit('initData')
}
},
}
</script>
<style scoped>
#content {
display: inline;
}
</style>
导入组件的使用示例
-
在需要使用到Excel文件导入功能的地方直接引入使用即可
-
我这里是将该组件注册成了全局的
import ImportExcel from '@/components/excel/ImportExcel'
Vue.component('ImportExcel', ImportExcel)
- 页面中使用
<import-excel @initData="initData" url="http://localhost:8090/sddonline/trainer/importExcel"></import-excel>
异步导出Excel的js文件
import axios from 'axios'
export default function(ids, url, fileName='表格数据') {
axios({
url: url,
method: 'post',
data: ids,
responseType: 'blob'
}).then(response => {
// 获取到blob文件数据
const data = response.data
// 将二进制文件转化为可访问的url
let url = window.URL.createObjectURL(data)
// 使用原生的document操作来创建一个a标签,并添加到body标签中
var a = document.createElement('a')
document.body.appendChild(a)
// 设置其href请求路径
a.href = url
// 设置好下载的文件名,需要带上后缀
a.download = fileName
// 模拟点击下载
a.click()
// 清除刚刚使用window.URL.createObjectURL(data) 创建的数据,避免在内存中遗留
window.URL.revokeObjectURL(url)
})
}
页面中编写按钮,完成异步导出功能
- 页面中的代码
<el-button type="primary" icon="el-icon-search" @click="exportExcel">导出excel</el-button>
- js代码
import exportExcelFile from '@/components/excel/exportExcel.js'
// 这里是页面中的一个按钮,那个按钮点击后调用该方法
exportExcel() {
// 封装已经选中的数据的id,如果没有选中,则导出全部,判断数组长度是否为0由后端处理
// 导出
let ids = [];
this.selectChangeValue.forEach(trainer => ids.push(trainer.id));
// 核心就是,调用这句话,然后把这个数组以json方式请求
exportExcelFile(ids, 'http://localhost:8090/sddonline/trainer/exportExcel', '培训师表格数据');
}
第11问:Vue配合ElementUI使用过程中的一些小问题
使用多级选择框时,清空子级选择框的内容
-
查看el-select的v-model绑定的值

-
将该绑定的值删除即可
delete this.courseVo.subjectId
为data中的模型数据添加属性无法被Vue监听到时
// 三个参数,第一个参数是需要操作的模型数据目标,
// 第二个参数,需要设置的key值
// 第三个参数,需要设置的key所对应的value
Vue.set(this.courseVo, 'cover', this.$store.state.app.ossBasePath + "/" + this.dataObj.key)
使用axios需要发送formData请求
- 安装qs
npm install qs
- 将需要进行提交的数据转换
axios({
url: '/sddonline/trainer/page',
method: 'post',
data: qs.stringify({a:'1',b:'2'})
})
将对象转换为JSON字符串,再转换成对象
- 使用qs与JSON进行比较
console.log(qs.stringify({a:'1',b:'2'})) // a=1&b=2 字符串
console.log(JSON.stringify({a:'1',b:'2'})) // {"a":"1","b":"2"} 字符串
console.log(qs.parse('a=1&b=2')) // {a: '1', b: '2'} 对象
console.log(JSON.parse('{"a":"1","b":"2"}')) // {a: '1', b: '2'} 对象
第12问:hutool工具类的常用功能
生成随机验证码
// 生成6位随机验证码
String code = RandomUtil.randomNumbers(6);
复制Bean的属性
session.setAttribute("user", BeanUtils.copyProperties(user,UserDTO.class));
response设置状态吗
response.setStatus(401);
将Object类型转换为指定类型
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
在Vue中,有分页的页面的列表如何显示序号
<el-table-column
label="序号"
width="100">
<template slot-scope="scope">
<!-- 插值表单式计算每页数据的序号 -->
{{ (pageNo - 1) * pageSize + scope.$index + 1 }}
</template>
</el-table-column>
第13问:SpringBoot项目瘦身,代码与jar分离打包
作用:就是将 SpringBoot 应用打包的 jar 利用合理的方式、方法减小体积。
- 引入依赖
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.6.1</version>
<configuration>
<mainClass>com.software.SoftWebApiApplication</mainClass>
<!--解决windows命令行窗口中文乱码-->
<jvmArguments>-Dfile.encoding=UTF-8</jvmArguments>
<layout>ZIP</layout>
<!--配置需要打包进项目的jar-->
<includes>
<!--这里是填写需要包含进去的jar,
必须项目中的某些模块,会经常变动,那么就应该将其坐标写进来
如果没有则non-exists ,表示不打包依赖
-->
<include>
<groupId>non-exists</groupId>
<artifactId>non-exists</artifactId>
</include>
</includes>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- 此插件用于将依赖包抽出-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
<!--是否排除传递性-->
<excludeTransitive>false</excludeTransitive>
<!--是否去掉 jar 包版本信息-->
<stripVersion>false</stripVersion>
<!--包含范围-->
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
- 如何运行
$ java -jar -Dloader.path="./lib" xxx-.jar
第14问:如何进行短信验证码的对接
1、打开微信公众平台,进行订阅号的注册
2、打开腾讯云,申请签名
3、腾讯云申请,申请短信模板
4、等待签名审核通过后,即可通过api来发送短信了
官方文档: https://cloud.tencent.com/document/product/382/43194
配置springboot的配置文件,添加SMS的参数信息
tencentsms:
secretId: AKIDCB0atvjYIE1Yq47xZ4Mnzvd8NN8ew1qO
secretKey: pPmumQ0ax75oIYaP4Vjz971rcCZhbHtK
region: ap-guangzhou
sdkAppId: 1400776968
signName: CodeStars的代码
templateId: 1632302
配置一个工具类
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.sms.v20210111.SmsClient;
import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author codeStars
* @date 2022/12/8 12:09
*/
@Component
@ConfigurationProperties(prefix = "tencentsms")
@Data
public class SMSUtils {
private String secretId;
private String secretKey;
private String region;
private String sdkAppId;
private String templateId;
private String signName;
/**
* 发送短信验证码
* @param phone 手机号
* @param code 验证码
*/
public void sendCodeMsg(String phone, String code) {
try {
/* 必要步骤:
* 实例化一个认证对象,入参需要传入腾讯云账户密钥对secretId,secretKey。
* 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。
* 你也可以直接在代码中写死密钥对,但是小心不要将代码复制、上传或者分享给他人,
* 以免泄露密钥对危及你的财产安全。
* SecretId、SecretKey 查询: https://console.cloud.tencent.com/cam/capi */
Credential cred = new Credential(secretId, secretKey);
// 实例化一个http选项,可选,没有特殊需求可以跳过
HttpProfile httpProfile = new HttpProfile();
// 设置代理(无需要直接忽略)
// httpProfile.setProxyHost("真实代理ip");
// httpProfile.setProxyPort(真实代理端口);
/* SDK默认使用POST方法。
* 如果你一定要使用GET方法,可以在这里设置。GET方法无法处理一些较大的请求 */
httpProfile.setReqMethod("POST");
/* SDK有默认的超时时间,非必要请不要进行调整
* 如有需要请在代码中查阅以获取最新的默认值 */
httpProfile.setConnTimeout(60);
/* 指定接入地域域名,默认就近地域接入域名为 sms.tencentcloudapi.com ,也支持指定地域域名访问,例如广州地域的域名为 sms.ap-guangzhou.tencentcloudapi.com */
httpProfile.setEndpoint("sms.tencentcloudapi.com");
/* 非必要步骤:
* 实例化一个客户端配置对象,可以指定超时时间等配置 */
ClientProfile clientProfile = new ClientProfile();
/* SDK默认用TC3-HMAC-SHA256进行签名
* 非必要请不要修改这个字段 */
clientProfile.setSignMethod("HmacSHA256");
clientProfile.setHttpProfile(httpProfile);
/* 实例化要请求产品(以sms为例)的client对象
* 第二个参数是地域信息,可以直接填写字符串ap-guangzhou,支持的地域列表参考 https://cloud.tencent.com/document/api/382/52071#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8 */
SmsClient client = new SmsClient(cred, region,clientProfile);
/* 实例化一个请求对象,根据调用的接口和实际情况,可以进一步设置请求参数
* 你可以直接查询SDK源码确定接口有哪些属性可以设置
* 属性可能是基本类型,也可能引用了另一个数据结构
* 推荐使用IDE进行开发,可以方便的跳转查阅各个接口和数据结构的文档说明 */
SendSmsRequest req = new SendSmsRequest();
/* 填充请求参数,这里request对象的成员变量即对应接口的入参
* 你可以通过官网接口文档或跳转到request对象的定义处查看请求参数的定义
* 基本类型的设置:
* 帮助链接:
* 短信控制台: https://console.cloud.tencent.com/smsv2
* 腾讯云短信小助手: https://cloud.tencent.com/document/product/382/3773#.E6.8A.80.E6.9C.AF.E4.BA.A4.E6.B5.81 */
/* 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId,示例如1400006666 */
// 应用 ID 可前往 [短信控制台](https://console.cloud.tencent.com/smsv2/app-manage) 查看
req.setSmsSdkAppId(sdkAppId);
/* 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名 */
// 签名信息可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-sign) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-sign) 的签名管理查看
req.setSignName(signName);
/* 模板 ID: 必须填写已审核通过的模板 ID */
// 模板 ID 可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-template) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-template) 的正文模板管理查看
req.setTemplateId(templateId);
/* 模板参数: 模板参数的个数需要与 TemplateId 对应模板的变量个数保持一致,若无模板参数,则设置为空 */
String[] templateParamSet = {code};
req.setTemplateParamSet(templateParamSet);
/* 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号]
* 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号 */
String[] phoneNumberSet = {"+86" + phone};
req.setPhoneNumberSet(phoneNumberSet);
/* 用户的 session 内容(无需要可忽略): 可以携带用户侧 ID 等上下文信息,server 会原样返回 */
String sessionContext = "";
req.setSessionContext(sessionContext);
/* 短信码号扩展号(无需要可忽略): 默认未开通,如需开通请联系 [腾讯云短信小助手] */
String extendCode = "";
req.setExtendCode(extendCode);
/* 国际/港澳台短信 SenderId(无需要可忽略): 国内短信填空,默认未开通,如需开通请联系 [腾讯云短信小助手] */
String senderid = "";
req.setSenderId(senderid);
/* 通过 client 对象调用 SendSms 方法发起请求。注意请求方法名与请求对象是对应的
* 返回的 res 是一个 SendSmsResponse 类的实例,与请求对象对应 */
SendSmsResponse res = client.SendSms(req);
// 输出json格式的字符串回包
System.out.println(SendSmsResponse.toJsonString(res));
// 也可以取出单个值,你可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义
// System.out.println(res.getRequestId());
/* 当出现以下错误码时,快速解决方案参考
* [FailedOperation.SignatureIncorrectOrUnapproved](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Afailedoperation.signatureincorrectorunapproved-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F)
* [FailedOperation.TemplateIncorrectOrUnapproved](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Afailedoperation.templateincorrectorunapproved-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F)
* [UnauthorizedOperation.SmsSdkAppIdVerifyFail](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Aunauthorizedoperation.smssdkappidverifyfail-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F)
* [UnsupportedOperation.ContainDomesticAndInternationalPhoneNumber](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Aunsupportedoperation.containdomesticandinternationalphonenumber-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F)
* 更多错误,可咨询[腾讯云助手](https://tccc.qcloud.com/web/im/index.html#/chat?webAppId=8fa15978f85cb41f7e2ea36920cb3ae1&title=Sms)
*/
} catch (TencentCloudSDKException e) {
throw new RuntimeException(e);
}
}
}
使用方式
@SpringBootTest
public class SMSTest {
@Autowired
private SMSUtils sMSUtils;
@Test
public void test() {
sMSUtils.sendCodeMsg("15314102250", "059845");
}
}
第16问: mybatisplus代码生成
导入依赖
<dependencies>
<!--。。。。。。。。。。。。。。。。。。。。代码生成临时使用。。。。。。。。。。。。。。。。。。。-->
<!-- mybaits-plus 代码生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.1</version>
</dependency>
<!-- 模板引擎 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
<!--引入mybatis-plus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!--mysql驱动包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
创建一个类用于自动生成
详细配置可以查看官网: https://baomidou.com/pages/981406/#可选配置
public class GeneratorCode {
public static void main(String[] args) {
System.gc();
Runtime.getRuntime().gc();
FastAutoGenerator.create(
"jdbc:mysql://localhost:3306/sdd_online_school", "root", "root")
.globalConfig(builder -> {
builder.author("codestars") // 设置作者
.enableSwagger() // 开启 swagger 模式
.fileOverride() // 覆盖已生成文件
.dateType(DateType.TIME_PACK)
.disableOpenDir()
.outputDir("D:\\auto"); // 指定输出目录
})
.packageConfig(builder -> {
builder.parent("com.woniu") // 设置父包名
.moduleName(null) // 设置父包模块名
.pathInfo(Collections.singletonMap(
OutputFile.mapperXml, "D:\\auto\\mapper")); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.entityBuilder()
.enableLombok()
//createTime属性设置新建数据时自动填充时间
.addTableFills(new Property("createTime", FieldFill.INSERT))
//updateTime属性设置插入以及更新的时候自动填充时间
.addTableFills(new Property("updateTime", FieldFill.INSERT_UPDATE))
//isDelete属性设置逻辑删除
.logicDeletePropertyName("isDeleted");
builder.controllerBuilder().enableHyphenStyle().enableRestStyle();
builder.mapperBuilder().enableBaseColumnList().enableBaseResultMap();
builder.addInclude("sdd_admin","sdd_trainer", "sdd_comment", "sdd_course", "sdd_course_collect", "sdd_course_description", "sdd_subject"
, "sdd_trainer", "sdd_video") // 设置需要生成的表名 trainer
.addTablePrefix("sdd_"); // 设置过滤表前缀
})
.execute();
}
}
第17问:SpringMVC使用过程中的问题
idea项目debug启动时,直接卡住
注意断点问题,大概率是断点导致
RequestBody无法接收到参数(实体类需要添加set方法)
后端明明已经设置好了跨域,但是仍然出现跨域请求403问题

第18问 Redis + Token解决接口幂等性问题
引入一些必备的依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.8</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
</dependencies>
修改配置文件
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://81.68.120.66:3306/cloud_product?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root
application:
name: product-server
redis:
host: 81.68.120.66
port: 6379
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: assign_id
server:
servlet:
context-path: /product
port: 8082
编写业务层接口以及实现类
接口
public interface IProductService extends IService<Product> {
void saveProduct(Product product);
String createToken();
}
实现类
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements IProductService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
@EnableIdempotentCheck
public void saveProduct(Product product) {
this.save(product);
}
@Override
public String createToken() {
String token = String.valueOf(IdUtil.getSnowflakeNextId());
stringRedisTemplate.opsForValue().set(token, "");
return token;
}
}
编写Controller接口
/**
* 响应一个保证幂等性的token
* @return
*/
@GetMapping("getIdempotentToken")
@ResponseBody
public R getIdempotentToken() {
String token = productService.createToken();
return R.ok().put("data", token);
}
@PostMapping("/save")
@ResponseBody
public R save(@RequestBody Product product) {
productService.saveProduct(product);
return R.ok("商品保存成功");
}
编写一个注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableIdempotentCheck {
/**
* 用户请求时,header 中我们的幂等性token的key
* @return
*/
String value() default "token";
}
编写一个切面
@Aspect
@Component
@Slf4j
public class IdempotentCheckAspect {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedissonClient redissonClient;
@Around(value = "@annotation(idempontentAnno)")
public Object aRound (ProceedingJoinPoint proceedingJoinPoint, EnableIdempotentCheck idempontentAnno) throws Throwable {
// 1、获取请求参数中的token
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
String token = servletRequestAttributes.getRequest().getHeader(idempontentAnno.value());
// 2、如果token不存在,代表请求非法
if (StringUtils.isBlank(token)) {
log.error("非法的请求,请求控制器为:{},请求方法为:{},请求参数为:",
proceedingJoinPoint.getTarget(),
proceedingJoinPoint.getSignature(),
proceedingJoinPoint.getArgs());
throw new IdempotentCheckException("非法的请求");
}
RLock lock = redissonClient.getLock(RedisConstant.PRODUCT_IDEMPOTENT_LOCK_KEY);
//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
boolean isLock = lock.tryLock();
if (isLock) {
try {
// 3、检查redis中是否有该key,如果不存在,则抛出异常
String redisToken = stringRedisTemplate.opsForValue().get(token);
if(Objects.isNull(redisToken)) {
log.error("重复的请求,请求控制器为:{},请求方法为:{},请求参数为:",
proceedingJoinPoint.getTarget(),
proceedingJoinPoint.getSignature(),
proceedingJoinPoint.getArgs());
throw new IdempotentCheckException("重复的请求");
}
// 4、执行目标方法
Object result = proceedingJoinPoint.proceed();
// 5、删除redis中的key
stringRedisTemplate.delete(token);
// 6、响应结果
return result;
}finally {
lock.unlock();
}
}
log.error("获取幂等性分布式锁失败,请求控制器为:{},请求方法为:{},请求参数为:",
proceedingJoinPoint.getTarget(),
proceedingJoinPoint.getSignature(),
proceedingJoinPoint.getArgs());
throw new IdempotentCheckException("幂等性校验时获取分布式锁失败");
}
}
编写一个异常类
public class IdempotentCheckException extends RuntimeException{
public IdempotentCheckException(String message) {
super(message);
}
}
编写一个Redis常量类
public class RedisConstant {
public static final String PRODUCT_IDEMPOTENT_LOCK_KEY = "product:idempotent_lock";
}
编写一个全局异常处理类
@RestControllerAdvice
@Slf4j
public class GlobalExceptionControllerAdvice {
/**
* 处理接口幂等性异常
* @param e
* @return
*/
@ExceptionHandler(IdempotentCheckException.class)
public R validateIdempotentCheckException(IdempotentCheckException e) {
log.error("接口幂等性校验错误,{},异常类型{}", e.getMessage(), e.getClass());
// 响应结果
return R.error(403,e.getMessage());
}
/**
* 处理数据校验异常。jsr303 数据格式校验
* @param e
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public R validateExceptionHandler(MethodArgumentNotValidException e) {
log.error("数据校验出现问题{},异常类型{}", e.getMessage(), e.getClass());
// 获取到所有的错误字段信息
BindingResult bindingResult = e.getBindingResult();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
// 将其封装到一个Map集合当中
Map<String, String> errorMap = new HashMap<>();
fieldErrors.forEach(fieldError -> errorMap.put(fieldError.getField(), fieldError.getDefaultMessage()));
// 响应结果
return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(), BizCodeEnum.VALID_EXCEPTION.getMessage()).put("data", errorMap);
}
/**
* 兜底异常处理
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
public R validateExceptionHandler(Exception e) {
log.error("系统出现异常{},异常类型{}", e.getMessage(), e.getClass());
e.printStackTrace();
// 响应结果
return R.error(BizCodeEnum.UNKNOWN_EXCEPTION.getCode(), BizCodeEnum.UNKNOWN_EXCEPTION.getMessage());
}
}
页面请求
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>添加商品</title>
<script type="text/javascript" th:src="@{/js/axios.min.js}"></script>
<script type="text/javascript" th:src="@{/js/vue.min.js}"></script>
</head>
<body>
<div id="app">
<form>
商品名称: <input name="productName" v-model="product.productName" type="text"> <br>
商品状态:
<select name="status" v-model="product.status">
<option value="0" label="已下架"></option>
<option value="1" label="已上架"></option>
</select>
<br>
单价: <input name="price" type="number" v-model="product.price"> <br>
描述: <textarea name="productDesc" v-model="product.productDesc"></textarea> <br>
标题:<input name="caption" type="text" v-model="product.caption"> <br>
库存: <input name="stock" type="number" v-model="product.stock"> <br>
<input type="button" @click="save" value="点击添加商品" />
</form>
</div>
<script>
window.onload=function(){
new Vue({
el: '#app',
data(){
return {
product: {},
token: ''
}
},
methods: {
save() {
axios({
url :'http://localhost:8082/product/product/save',
method : 'POST',
data: this.product,
headers : {
token: this.token
}
}).then(res => {
alert(res.data.msg)
})
}
},
created() {
// 获取幂等性token
axios.get('http://localhost:8082/product/product/getIdempotentToken').then(res => {
this.token = res.data.data;
})
}
})
}
</script>
</body>
</html>
第19问 分布式定时任务调度xxl-job的使用
1、先去github或者gitee下载调度中心
2、修改里面的配置,例如链接数据库的
3、创建一个工程,连接到该调度中心
调度中心部署(注意其中的accessToken)
- 先从git上拉取项目代码
github:https://github.com/xuxueli/xxl-job
gitee:https://gitee.com/xuxueli0323/xxl-job
拉取后在doc文件夹下,可以看到项目的官方文档,以及项目运行所需要使用的数据库,在本地的数据库中运行相应的sql文件。

xxl-job-admin模块是整个项目的控制台,可以抽象的理解为类似于微服务架构中的注册中心。
xxl-job-executor-sample模块就是定时任务的执行器模块,相当于是服务提供者,所有的定时任务都是需要通过执行器执行的,因此我们在使用的时候就需要创建相应的任务执行器,然后需要将执行器注册到admin控制台中,才能进行后续的使用。
-
设置模板xxl-job-admin中的application.properties的服务端口,数据库信息和邮件配置信息。
-
启动xxl-job-admin模块,启动成功后通过浏览器访问控制台页面
http://localhost:8080/xxl-job-admin。进入登录页面,初始化的账户信息为:username:admin pwd:123456

执行器管理菜单:也就是我们需要创建对应的任务执行器来执行任务
任务管理:自定义任务,并配置任务执行的时机
开发执行器
- 随意创建一个工程,引入如下依赖
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.3.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>
- 修改配置文件
xxl:
job:
### 执行器通讯TOKEN [选填]:非空时启用;
accessToken: default_token
admin:
### 调度中心部署根地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
addresses: http://localhost:8080/xxl-job-admin
executor:
### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
address: ''
### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
appname: ${spring.application.name}
### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";
ip: ''
### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
logpath: /data/applogs/xxl-job/jobhandler
### 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;
logretentiondays: 30
### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
port: 9999
spring:
application:
name: test-executor1
server:
port: 8081
- 添加一个配置类
@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(com.codestars.config.XxlJobConfig.class);
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.appname}")
private String appname;
@Value("${xxl.job.executor.address}")
private String address;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
/**
* 针对多网卡、容器内部署等情况,可借助 "spring-cloud-commons" 提供的 "InetUtils" 组件灵活定制注册IP;
*
* 1、引入依赖:
* <dependency>
* <groupId>org.springframework.cloud</groupId>
* <artifactId>spring-cloud-commons</artifactId>
* <version>${version}</version>
* </dependency>
*
* 2、配置文件,或者容器启动变量
* spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.'
*
* 3、获取IP
* String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
*/
}
- 创建一个任务处理器
@Component
public class MyTesk {
@XxlJob("jobHandler1")
public void jobHandler1 () {
System.out.println("执行了一次定时任务,嘿嘿,111");
}
}
-
启动项目,在调度中心添加一个执行器

-
耐心等待一下,等待执行器成功注册到调度中心

-
在调度中心的左侧菜单选择任务管理 > 新增任务

-
使任务执行一次或按照cron表达式启动

任务的Bean运行模式与GLUE的运行区别
Bean模式: 通过jobHandler来执行任务
GLUE模式: 可以执行任意类的任意方法执行,因为是自己编写代码
Glue示例
-
创建一个GLUE任务

-
编写代码执行指定类的指定方法

package com.xxl.job.service.handler;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.IJobHandler;
import com.codestars.task.MyTesk;
public class DemoGlueJobHandler extends IJobHandler {
@Override
public void execute() throws Exception {
MyTesk myTesk = new MyTesk();
myTesk.jobHandler1();
}
}
搭建集群环境并测试分片广播方式执行任务(执行器中的多个节点共同执行定时任务)
- 复制一个项目的配置,然后修改其启动参数,分别为
# 第一台
-Dxxl.job.executor.port=9998 -Dserver.port=8081
# 第二台
-Dxxl.job.executor.port=9999 -Dserver.port=8082
- 修改任务,使其获取到分片信息
@Component
public class MyTesk {
@XxlJob("jobHandler1")
public void jobHandler1 () {
int shardIndex = XxlJobHelper.getShardIndex();
int shardTotal = XxlJobHelper.getShardTotal();
System.out.println("当前分片索引为: " + shardIndex);
System.out.println("当前分片总数为: " + shardTotal);
// 此时可以这么执行分片任务
String sql = "select id from student where mod(id, #{shardTotal}) = #{shardIndex}";
System.out.println("执行了一次定时任务,嘿嘿,111");
}
}
-
添加任务并指定为分片广播

-
执行任务查看效果



浙公网安备 33010602011771号