Java笔记-23、Web后端实战-员工管理-新增员工
新增员工
- 保存员工基本数据
- 批量保存员工工作经历
MyBatis动态SQL-循环遍历
- 在resourses下新建同包同名xml。
- 使用
<foreach>标签。
属性说明:
collection:集合名称item: 集合遍历出来的元素/项separator: 每一次遍历使用的分隔符open:遍历开始前拼接的片段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插入基本信息的方法上加入注解:
useGeneratedKeys:表示使用生成的主键。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
事务一旦提交或回滚,它对数据库中的数据的改变就是永久的
文件上传
简介
文件上传:是指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程。
文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。
前端三要素
- method要定义为post
enctype="multipart/form-data"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
- FastDFS MinIO 搭建文件服务。
- 使用云存储。
对象存储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();
}
}
}
}
将上面的 endpoint ,bucketName,objectName,file 都需要改成自己的。
在以上代码中,需要替换的内容为:
- endpoint:阿里云OSS中的bucket对应的域名
- bucketName:Bucket名称
- objectName:对象名称,在Bucket中存储的对象的名称
- region:bucket所属区域
集成
在新增员工的时候,上传员工的图像,而之所以需要上传员工的图像,是因为将来我们需要在系统页面当中访问并展示员工的图像。而要想完成这个操作,需要做两件事:
- 需要上传员工的图像,并把图像保存起来(存储到阿里云OSS)
- 访问员工图像(通过图像在阿里云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提供的简化方式套路:
- 需要创建一个实现类,且实体类中的属性名和配置文件当中key的名字必须要一致
比如:配置文件当中叫endpoint,实体类当中的属性也得叫endpoint,另外实体类当中的属性还需要提供 getter / setter方法
- 需要将实体类交给Spring的IOC容器管理,成为IOC容器当中的bean对象
- 在实体类上添加
@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();
...
}
...
}

浙公网安备 33010602011771号