Java笔记-23、Web后端实战-员工管理-新增员工

新增员工

  1. 保存员工基本数据
  2. 批量保存员工工作经历

MyBatis动态SQL-循环遍历

  1. 在resourses下新建同包同名xml。
  2. 使用<foreach>标签。

属性说明:

  1. collection:集合名称
  2. item: 集合遍历出来的元素/项
  3. separator: 每一次遍历使用的分隔符
  4. open:遍历开始前拼接的片段
  5. close:遍历结束后拼接的片段
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.study.mapper.EmpExprMapper">


    <insert id="insertBatch">
        insert into emp_expr(emp_id, begin, end, company, job) values
        <foreach collection="exprList" item="expr" separator=",">
            (#{expr.empId},#{expr.begin},#{expr.end},#{expr.company},#{expr.job})
        </foreach>

    </insert>
</mapper>

MyBatis主键返回

现在的问题是,工作经历需要emp_id这个字段标识工作经历属于哪个员工,但emp_id也就是emp的id是通过数据库主键自增得到的,前端并没有给这一字段值。要想在程序里快速得到这一字段值,需要用到MyBatis的主键返回注解。

在EmpMapper插入基本信息的方法上加入注解:

  1. useGeneratedKeys:表示使用生成的主键。
  2. keyProperty:表示将生成的主键赋值给emp对象的id属性
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("insert into emp(username, name, gender, phone, job, salary, image, entry_date, dept_id, create_time, update_time) " +
        "values (#{username}, #{name}, #{gender}, #{phone}, #{job}, #{salary}, #{image}, #{entryDate}, #{deptId}, #{createTime}, #{updateTime})")
void insert(Emp emp);

此时,当Service中执行完empMapper.insert(emp);后,emp的属性id就有值了,就可以遍历集合为exprList中的empId赋值了。

    @Override
    public void save(Emp emp) {
        emp.setCreateTime(LocalDateTime.now());
        emp.setUpdateTime(LocalDateTime.now());
        empMapper.insert(emp); //执行完emp.id就有值了

        List<EmpExpr> exprList = emp.getExprList();
        if(!CollectionUtils.isEmpty(exprList)){
            exprList.forEach(empExpr -> {
                empExpr.setEmpId(emp.getId());
            });
            empExprMapper.insertBatch(exprList);
        }
        
    }

事务管理

保存员工的基本信息成功了,而保存工作经历失败了,这是不对的。

保存信息是一个业务操作,只有一部分完成是不行的。

介绍、操作

概念:事务 是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作 要么同时成功,要么同时失败。

默认MySQL的事务是自动提交的,也就是说,当执行一条DML语句,MySQL会立即隐式的提交事务,有可能导致保存员工的基本信息成功了,而保存工作经历失败了。

事务控制主要三步操作:开启事务、提交事务/回滚事务。

-- 开启事务
start transaction;/ begin;

-- 业务1

-- 业务2

-- 要么提交事务(全部成功)/要么回滚事务(有一个失败)
commit;/ rollback;

使用场景:银行转账、下单扣减库存。

Spring事务管理

注解:@Transactional

作用:将当前方法交给Spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务。

位置:业务(service)层的方法上、类上、接口上。

推荐将这个注解加到方法上,避免资源浪费。

使用时机:在一个业务方法中多次对数据库的数据进行增删改。

为方便看到spring事务管理的底层日志,可以在application.yml中单独配置。

logging:
  level:
    org.springframework.jdbc.support.JdbcTransactionManager: debug

grep console插件可以让控制台输出更明显。

事务进阶-rollbackFor和propagation

Transactional注解提供rollbackFor属性,用于控制出现何种异常类型,才回滚事务。

Transactional注解默认出现运行时异常RuntimeException才会回滚。

如果要所有的异常都回滚,则用到rollbackFor

@Transacitonal(rollbackFor={Excption.class})

Transactional注解提供propagation属性。

事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。

当一个事务方法调用另一个事务方法时,被调用的事务方法是新建一个事务还是加入到调用方法的事务中呢?需要在被调用的方法注解中加上属性propagation

属性值 含义
REQUIRED 【默认值】需要事务,有则加入,无则创建新事务
REQUIRES_NEW 需要新事务,无论有无,总是创建新事务
SUPPORTS 支持事务,有则加入,无则在无事务状态中运行
NOT_SUPPORTED 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务
MANDATORY 必须有事务,否则抛异常
NEVER 必须没事务,否则抛异常

事务的四大特性(ACID)

原子性

Atomicity

事务是不可分割的最小单元,要么全部成功,要么全部失败

一致性

Consistency

事务完成时,必须使所有的数据都保持一致状态

隔离性

Isolation

数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行

持久性

Durability

事务一旦提交或回滚,它对数据库中的数据的改变就是永久的

文件上传

简介

文件上传:是指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程。

文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。

前端三要素

  1. method要定义为post
  2. enctype="multipart/form-data"
  3. type="file"
<form action="/upload" method="post" enctype="multipart/form-data">
 姓名:<input type="text" namesfname" ><br>
 年龄:<input type="text" name="age" ><br>
 图像:<input type="file" name="file" ><br>
<input type="submit" value="上传文件"name="submit">
</form>

后端接收文件

对应前端的图像的name="file",在后端用MultipartFile file对象进行接收。(名)

@SLf4j
@RestController
pubLic class UploadController{

  @PostMapping("/upload")
  public Result handleFileUpload(String name,Integer age, MultipartFile file){
  log.info("文件上传:{}",file);
  return Result.success();
  }

}

上传的文件存储在临时文件夹下,一旦响应完毕,临时文件夹下的文件就删除了。就需要将文件保存下来。

本地存储

@Slf4j
@RestController
public class UploadController {

    @PostMapping("/upload")
    public Result upload(String name, Integer age, MultipartFile file) throws Exception {
        log.info("接收参数:{},{},{}", name, age, file);
        // 获取原始文件名
        String originalFilename = file.getOriginalFilename();

        // 保存文件
        file.transferTo(new File("~/Downloads/images/" + originalFilename));
        return Result.success();
    }

}

使用MultipartFile的方法获取原始文件名,并new一个file使用transferTo方法存到本地。

上述的问题:同名文件会覆盖。

如何解决?

方法之一:使用UUID生成唯一字符串。

UUID.randomUUID().toString()

// 新文件名方法一
 String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
 String fileName = UUID.randomUUID().toString() + extension;

// 保存文件
file.transferTo(new File("~/Downloads/images/" + fileName));

spring默认上传文件最大大小为1MB。

在application.yml中解除限制:

spring:
  servlet:
    multipart:
      max-file-size: 10MB  # 最大单个文件大小
      max-request-size: 100MB  # 最大请求总大小(文件+表单数据)

OSS

  1. FastDFS MinIO 搭建文件服务。
  2. 使用云存储。

对象存储OSS (Object Storage Service),是一款海量、安全、低成本、高可靠的云存储服务。使用OSS,可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。

第三方服务-通用思路

准备工作->参照官方SDK编写入门程序->集成使用

SDK: Software Development Kit 的缩写,软件开发工具,包括辅助软件开发的依赖(jar包)、代码示例等,都可以叫做SDK。

准备工作

开通对象存储服务(OSS)->创建bucket->获取并配置AccessKey(秘钥)

Bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间。

配置AccessKey

管理员身份打开CMD命令行,执行如下命令,配置系统的环境变量。

set OSS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
set OSS_ACCESS_KEY_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

执行如下命令,让更改生效。

setx OSS_ACCESS_KEY_ID "%OSS_ACCESS_KEY_ID%"
setx OSS_ACCESS_KEY_SECRET "%OSS_ACCESS_KEY_SECRET%"

执行如下命令,验证环境变量是否生效。

echo %OSS_ACCESS_KEY_ID%
echo %OSS_ACCESS_KEY_SECRET%
入门程序
<!--阿里云OSS依赖-->
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.17.4</version>
</dependency>

<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>
<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
    <version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.3.3</version>
</dependency>
package com.itheima;

import com.aliyun.oss.*;
import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider;
import com.aliyun.oss.common.comm.SignVersion;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.nio.file.Files;

public class Demo {

    public static void main(String[] args) throws Exception {
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "https://oss-cn-beijing.aliyuncs.com";
        // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
        EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "java-ai";
        // 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
        String objectName = "001.jpg";
        // 填写Bucket所在地域。以华东1(杭州)为例,Region填写为cn-hangzhou。
        String region = "cn-beijing";

        // 创建OSSClient实例。
        ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
        clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
        OSS ossClient = OSSClientBuilder.create()
            .endpoint(endpoint)
            .credentialsProvider(credentialsProvider)
            .clientConfiguration(clientBuilderConfiguration)
            .region(region)
            .build();

        try {
            File file = new File("C:\\Users\\deng\\Pictures\\1.jpg");
            byte[] content = Files.readAllBytes(file.toPath());

            ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content));
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }
}

将上面的 endpointbucketNameobjectNamefile 都需要改成自己的。

在以上代码中,需要替换的内容为:

  • endpoint:阿里云OSS中的bucket对应的域名
  • bucketName:Bucket名称
  • objectName:对象名称,在Bucket中存储的对象的名称
  • region:bucket所属区域
集成

在新增员工的时候,上传员工的图像,而之所以需要上传员工的图像,是因为将来我们需要在系统页面当中访问并展示员工的图像。而要想完成这个操作,需要做两件事:

  1. 需要上传员工的图像,并把图像保存起来(存储到阿里云OSS)
  2. 访问员工图像(通过图像在阿里云OSS的存储地址访问图像)
    • OSS中的每一个文件都会分配一个访问的url,通过这个url就可以访问到存储在阿里云上的图片。所以需要把url返回给前端,这样前端就可以通过url获取到图像。

1). 引入阿里云OSS上传文件工具类(由官方的示例代码改造而来)

@Component
public class AliyunOSSOperator {

    private String endpoint = "https://oss-cn-beijing.aliyuncs.com";
    private String bucketName = "java-ai";
    private String region = "cn-beijing";

    public String upload(byte[] content, String originalFilename) throws Exception {
        // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
        EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();

        // 填写Object完整路径,例如202406/1.png。Object完整路径中不能包含Bucket名称。
        //获取当前系统日期的字符串,格式为 yyyy/MM
        String dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));
        //生成一个新的不重复的文件名
        String newFileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));
        String objectName = dir + "/" + newFileName;

        // 创建OSSClient实例。
        ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
        clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
        OSS ossClient = OSSClientBuilder.create()
                .endpoint(endpoint)
                .credentialsProvider(credentialsProvider)
                .clientConfiguration(clientBuilderConfiguration)
                .region(region)
                .build();

        try {
            ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content));
        } finally {
            ossClient.shutdown();
        }

        return endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + objectName;
    }

}

2). 修改UploadController代码

@Slf4j
@RestController
public class UploadController {
    
    @Autowired
    private AliyunOSSOperator aliyunOSSOperator;

    @PostMapping("/upload")
    public Result upload(MultipartFile file) throws Exception {
        log.info("上传文件:{}", file);
        if (!file.isEmpty()) {
            // 生成唯一文件名
            String originalFilename = file.getOriginalFilename();
            String extName = originalFilename.substring(originalFilename.lastIndexOf("."));
            String uniqueFileName = UUID.randomUUID().toString().replace("-", "") + extName;
            // 上传文件
            String url = aliyunOSSOperator.upload(file.getBytes(), uniqueFileName);
            return Result.success(url);
        }
        return Result.error("上传失败");
    }

}

只是较简单的示例,有很多安全问题,没做限制。

自定义参数值的注入
private String endpoint = "https://oss-cn-beijing.aliyuncs.com";
private String bucketName = "java-ai";
private String region = "cn-beijing";

不能写死!

application.yml

#阿里云OSS
aliyun:
  oss:
    endpoint: https://oss-cn-beijing.aliyuncs.com
    bucketName: java-ai
    region: cn-beijing
@Component
public class AliyunOSSOperator {

    //方式一: 通过@Value注解一个属性一个属性的注入
    @Value("${aliyun.oss.endpoint}")
    private String endpoint;
    
    @Value("${aliyun.oss.bucketName}")
    private String bucketName;
    
    @Value("${aliyun.oss.region}")
    private String region;

    ....
}

如果只有一两个属性需要注入,而且不需要考虑复用性,使用@Value注解就可以了。

但是使用@Value注解注入配置文件的配置项,如果配置项多,注入繁琐,不便于维护管理 和 复用。

spring提供的自定义参数注入简化方法

Spring提供的简化方式套路:

  1. 需要创建一个实现类,且实体类中的属性名和配置文件当中key的名字必须要一致

比如:配置文件当中叫endpoint,实体类当中的属性也得叫endpoint,另外实体类当中的属性还需要提供 getter / setter方法

  1. 需要将实体类交给Spring的IOC容器管理,成为IOC容器当中的bean对象
  2. 在实体类上添加@ConfigurationProperties注解,并通过prefix属性来指定配置参数项的前缀
定义实体类

定义实体类AliyunOSSProperties ,并交给IOC容器管理

@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliyunOSSProperties {
    private String endpoint;
    private String bucketName;
    private String region;
}
注入并使用
@Component
public class AliyunOSSOperator {
    @Autowired
    private AliyunOSSProperties aliyunOSSProperties;

    public String upload(byte[] content, String originalFilename) throws Exception {
        String endpoint = aliyunOSSProperties.getEndpoint();
        String bucketName = aliyunOSSProperties.getBucketName();
        String region = aliyunOSSProperties.getRegion();
        ...
    }
    ...
}
posted @ 2025-04-02 22:49  subeipo  阅读(42)  评论(0)    收藏  举报