在实际开发中遇到的各种问题解决方案

目录

第一问:使用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功能
    image
    image
    image

  • 创建一个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

image

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(设置公共读)

image

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

EnterPoint访问端点

image

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

image
image

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

image
image

使用原始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)
  • 在需要校验的实体类中的字段,可以添加如下注解
    image
	@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();
    }

image

分组数据校验

创建几个接口,接口中什么东西都不用添加

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的控制台并创建桶子(注意桶子需要设置为公共读)

image
image
image

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
  }
}
文件上传成功后将会得到如下响应

image

第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绑定的值
    image

  • 将该绑定的值删除即可

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问题

image

第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)

  1. 先从git上拉取项目代码
    github:https://github.com/xuxueli/xxl-job
    gitee:https://gitee.com/xuxueli0323/xxl-job

拉取后在doc文件夹下,可以看到项目的官方文档,以及项目运行所需要使用的数据库,在本地的数据库中运行相应的sql文件。
image

xxl-job-admin模块是整个项目的控制台,可以抽象的理解为类似于微服务架构中的注册中心。

xxl-job-executor-sample模块就是定时任务的执行器模块,相当于是服务提供者,所有的定时任务都是需要通过执行器执行的,因此我们在使用的时候就需要创建相应的任务执行器,然后需要将执行器注册到admin控制台中,才能进行后续的使用。

  1. 设置模板xxl-job-admin中的application.properties的服务端口,数据库信息和邮件配置信息。

  2. 启动xxl-job-admin模块,启动成功后通过浏览器访问控制台页面http://localhost:8080/xxl-job-admin。进入登录页面,初始化的账户信息为:username:admin pwd:123456

image
执行器管理菜单:也就是我们需要创建对应的任务执行器来执行任务
任务管理:自定义任务,并配置任务执行的时机

开发执行器

  1. 随意创建一个工程,引入如下依赖
    <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>
  1. 修改配置文件
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
  1. 添加一个配置类
@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();
     */


}
  1. 创建一个任务处理器
@Component
public class MyTesk {

    @XxlJob("jobHandler1")
    public void jobHandler1 () {
        System.out.println("执行了一次定时任务,嘿嘿,111");
    }
}
  1. 启动项目,在调度中心添加一个执行器
    image

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

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

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

任务的Bean运行模式与GLUE的运行区别

Bean模式: 通过jobHandler来执行任务

GLUE模式: 可以执行任意类的任意方法执行,因为是自己编写代码

Glue示例

  1. 创建一个GLUE任务
    image

  2. 编写代码执行指定类的指定方法
    image

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");
    }
}

  • 添加任务并指定为分片广播
    image

  • 执行任务查看效果
    image
    image

第20问 禅道系统的学习与使用

管理员、产品经理、项目经理、研发、测试

posted @ 2022-10-11 12:54  CodeStars  阅读(1304)  评论(0)    收藏  举报