# Optional
传统的写代码方式经常会遇到NullPointerException,这就需要我们在代码中经常判空。而判空的写法又会显得很累赘,这里就可以用到Optional来简化代码。
- Optional是在java.util包下的一个用于代替null的一个工具类
- Optional 是个容器:它可以保存类型T的值,或者仅仅保存null
- Optional 类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象
Optional 类的引入很好的解决空指针异常的问题
# 实现可选参数的功能
javaCopy codepublic void myMethod(int a, Optional<Integer> b) {
int value = b.orElse(10); // 使用默认值 10,如果提供了参数 b,则使用提供的值
// 使用参数 a 和 value 进行处理逻辑
}
# 构建Optional对象
- 创建一个空的 Optional 实例
Optional<String> emptyOptional = Optional.empty();
- 创建一个 Optional 实例,当 t为null时,抛出NullPointerException异常
Optional<String> notNullOptional = Optional.of("aaa");
- 创建一个 Optional 实例,但当 t为null时不会抛出异常,而是返回一个空的实例
Optional<String> notNullOptional = Optional.ofNullable("aaa");
# 相关方法使用
- isPresent():持有非空值,返回true;否则false;
Optional optional = Optional.ofNullable(null);
Optional optional1 = Optional.of("");
Optional optional2 = Optional.empty();
System.out.println(optional.isPresent()); //fasle
System.out.println(optional1.isPresent()); //true
System.out.println(optional2.isPresent()); //false
- ifPresent():如果 Optional 中有值,返回该值,否则什么也不做
Optional<String> optional = Optional.of("Hello optional");
System.out.println("optional get is :"+optional.get());
optional.ifPresent(System.out::println);
- orElse:参数是一个值,如果 Optional 中有值则将其返回,否则返回 orElse 方法传入的参数
//Optional 中有值
Optional optional = Optional.ofNullable("fdgshsgf");
System.out.println(optional.orElse("reiwgybv"));
- orElseGet:功能与orElse一样,只不过orElseGet参数是一个对象
optional.orElseGet(() -> "Default Value")
- orElseThrow:如果optional不为空,则返回optional中的对象;如果为null,则抛出Supplier函数生成的异常
String bb = optional.orElseThrow(() -> new Exception("抛出异常"));
- map:为空返回Optional.empty,否则返回一个新的Optional,函数mapper在以value作为输入时的输出值可以多次使用map操作
Optional<String> username = Optional.ofNullable(getUserById(id))
.map(user -> user.getUsername())
.map(name -> name.replace('_', ' '));
System.out.println("Username is: " + username.orElse("Unknown"));
- flatMap:map 方法参数中的函数 mapper 输出的是值,然后 map 方法会使用 Optional.ofNullable 将其包装为 Optional;而 flatMap 要求参数中的函数 mapper 输出的就是 Optional
Optional<String> username = Optional.ofNullable(getUserById(id))
.flatMap(user -> Optional.of(user.getUsername()))
.flatMap(name -> Optional.of(name.toLowerCase()));
System.out.println("Username is: " + username.orElse("Unknown"));
- filter:如果有值并且满足条件,就返回该Optional,否则返回空Optional
Optional<User> result = Optional.ofNullable(user).filter(u -> u.getEmail().contains("@"));
# SPI 机制
SPI 全称为 Service Provider Interface,是一种服务发现机制
# SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类
# 这样可以在运行时,动态为接口替换实现类
# 示例代码
// 定义一个接口,名称为 Robot
public interface Robot {
void sayHello();
}
// 定义两个实现类,分别为 OptimusPrime 和 Bumblebee
public class OptimusPrime implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Optimus Prime.");
}
}
public class Bumblebee implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Bumblebee.");
}
}
# Java SPI 示例
在 META-INF/services 文件夹下创建一个文件,名称为 Robot 的全限定名 org.apache.spi.Robot。文件内容为实现类的全限定的类名,如下:
org.apache.spi.OptimusPrime
org.apache.spi.Bumblebee
编写代码进行测试:
public class JavaSPITest {
@Test
public void sayHello() throws Exception {
// 使用 SPI 来获取驱动的实现类
ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
System.out.println("Java SPI");
// 1. forEach 模式
serviceLoader.forEach(Robot::sayHello);
// 2. 迭代器模式
Iterator<Robot> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
Robot robot = iterator.next();
robot.sayHello();
}
// 输出
// Java SPI
// Hello, I am Optimus Prime.
// Hello, I am Bumblebee.
}
}
# Spring SPI 机制
在资源文件目录,创建一个固定的文件 META-INF/spring.factories
#key是接口的全限定名,value是接口的实现类
org.apache.spi.Robot = org.apache.spi.OptimusPrime,org.apache.spi.Bumblebee
运行代码:
// 调用 SpringFactoriesLoader.loadFactories 方法加载 Robot 接口所有实现类的实例
List<Robot> myTestServices = SpringFactoriesLoader.loadFactories(
Robot.class,
Thread.currentThread().getContextClassLoader()
);
for (Robot testService : myTestServices) {
testService.sayHello();
}
注意
和 Java SPI 一样,Spring SPI 也无法获取某个固定的实现,只能按顺序获取所有实现
# MapStruct
随着微服务和分布式应用程序迅速占领开发领域,数据完整性和安全性比以往任何时候都更加重要。在这些松散耦合的系统之间,安全的通信渠道和有限的数据传输是最重要的。大多数时候,终端用户或服务不需要访问模型中的全部数据,而只需要访问某些特定的部分。
数据传输对象(Data Transfer Objects, DTO)经常被用于这些应用中。DTO只是持有另一个对象中被请求的信息的对象。通常情况下,这些信息是有限的一部分。例如,在持久化层定义的实体和发往客户端的DTO之间经常会出现相互之间的转换。由于DTO是原始对象的反映,因此这些类之间的映射器在转换过程中扮演着关键角色。
这就是MapStruct解决的问题:手动创建bean映射器非常耗时。 但是该库可以自动生成Bean映射器类。
# 简单使用
- 引入依赖,注意:当lombok和mapstruct一起用的时候,会导致mapstruct失效
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.3.Final</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version> 1.18.28</version>
</dependency>
<!--MapStruct在编译时工作,并且会集成到像Maven和Gradle这样的构建工具上-->
<!--所以必须在添加插件maven-compiler-plugin-->
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<!--注意lombok和mapstruct的顺序-->
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version> 1.18.28</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.3.Final</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
- 创建 DTO、VO
//DTO
@Data
public class StudentDto {
private String userName;
private String userId;
private String address;
private String school;
private int age;
private String email;
}
//VO
@Data
@Builder
public class StudentVo {
private String userName;
private String userId;
private String address;
private String school;
private int age;
private String emailAddress;
}
- 创建mapstruct转换器
//componentModel = "spring" 交给spring管理
@Mapper(componentModel = "spring")
public interface MainMapper {
StudentDto studentVo2Dto(StudentVo vo);
}
- 编写测试用例
@SpringBootTest
class SpringbootMapstructApplicationTests {
@Autowired
private MainMapper mainMapper;
@Test
void testSimpleMap() {
StudentVo studentVo = StudentVo.builder()
.school("清华大学")
.userId("ams")
.userName("AI码师")
.age(27)
.address("合肥")
.build();
StudentDto studentDto = mainMapper.studentVo2Dto(studentVo);
System.out.println(studentDto);
}
}
- 查看MainMapper生成的代码
# 不同字段映射
@Mapper
public interface DoctorMapper {
//Doctor中的specialty字段对应于DoctorDto类的 specialization
@Mapping(source = "doctor.specialty", target = "specialization")
DoctorDto toDto(Doctor doctor);
}
# 多个数据源类
有时,单个类不足以构建DTO,我们可能希望将多个类中的值聚合为一个DTO,供终端用户使用
@Data
public class Doctor {
private int id;
private String name;
private String specialty;
}
@Data
public class Education {
private String degreeName;
private String institute;
private Integer yearOfPassing;
}
@Data
public class DoctorDto {
private int id;
private String name;
private String degree;
private String specialization;
}
接下来,将 DoctorMapper 接口更新为如下代码:
@Mapper
public interface DoctorMapper {
//如果Education和Doctor包含同名的字段,必须让映射器知道使用哪一个,否则它会抛出一个异常
@Mapping(source = "doctor.specialty", target = "specialization")
@Mapping(source = "education.degreeName", target = "degree")
DoctorDto toDto(Doctor doctor, Education education);
}
# 子对象映射
多数情况下,POJO中不会只包含基本数据类型,其中往往会包含其它类。比如说,一个Doctor类中会有多个患者类:
public class Patient {
private int id;
private String name;
}
在Doctor中添加一个患者列表List:
public class Doctor {
private int id;
private String name;
private String specialty;
private List<Patient> patientList;
}
因为Patient需要转换,为其创建一个对应的DTO:
public class PatientDto {
private int id;
private String name;
}
最后,在 DoctorDto 中新增一个存储 PatientDto的列表:
public class DoctorDto {
private int id;
private String name;
private String degree;
private String specialization;
private List<PatientDto> patientDtoList;
}
在修改 DoctorMapper之前,我们先创建一个支持 Patient 和 PatientDto 转换的映射器接口:
@Mapper
public interface PatientMapper {
PatientDto toDto(Patient patient);
}
然后,我们再来修改 DoctorMapper 处理一下患者列表:
//因为我们要处理另一个需要映射的类,所以这里设置了@Mapper注解的uses标志
@Mapper(uses = {PatientMapper.class})
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization")
DoctorDto toDto(Doctor doctor);
}
# List集合映射
@Mapper(componentModel = "spring")
public interface MainMapper {
@Mapping(source = "emailAddress", target = "email")
StudentDto studentVo2Dto(StudentVo vo);
List<StudentDto> studentListVo2Dto(List<StudentVo> vo);
}
# Set和Map映射
@Mapper
public interface DoctorMapper {
Set<DoctorDto> setConvert(Set<Doctor> doctor);
Map<String, DoctorDto> mapConvert(Map<String, Doctor> doctor);
}
# Map和JavaBean的映射
@Mapper
public interface DoctorMapper {
Doctor toBean(Map<String,String> map);
}
//测试
HashMap<String, String> stringObjectHashMap = new HashMap<>();
stringObjectHashMap.put("id","10");
stringObjectHashMap.put("name","lisi");
Doctor doctor1 = doctorMapper.toBean(stringObjectHashMap);
System.out.println(doctor1);
# 数据类型转换
- 时间格式转换
public class PatientDto {
private int id;
private String name;
private LocalDate dateOfBirth;
}
public class Patient {
private int id;
private String name;
private String dateOfBirth;
}
//映射器
@Mapper
public interface PatientMapper {
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
Patient toModel(PatientDto patientDto);
}
- 数字格式转换
@Mapping(source = "price", target = "price", numberFormat = "$#.00")
- 枚举格式转换
public enum PaymentType {
CASH,
CHEQUE,
CARD_VISA,
CARD_MASTER,
CARD_CREDIT
}
public enum PaymentTypeView {
CASH,
CHEQUE,
CARD
}
//两个enum之间的映射器接口
@Mapper
public interface PaymentTypeMapper {
@ValueMappings({
@ValueMapping(source = "CARD_VISA", target = "CARD"),
@ValueMapping(source = "CARD_MASTER", target = "CARD"),
@ValueMapping(source = "CARD_CREDIT", target = "CARD")
})
PaymentTypeView paymentTypeToPaymentTypeView(PaymentType paymentType);
}
# 添加默认值
@Mapping 注解有两个很实用的标志就是常量 constant 和默认值 defaultValue
@Mapper(uses = {PatientMapper.class}, componentModel = "spring")
public interface DoctorMapper {
@Mapping(target = "id", constant = "-1")
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization", defaultValue = "aa")
DoctorDto toDto(Doctor doctor);
}
# 文件头类型校验
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.0.M3</version>
</dependency>
//文件上传拦截器
public class FileInterceptor implements HandlerInterceptor {
Logger log = LoggerFactory.getLogger(getClass());
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
// 判断是否为文件上传请求
if (req instanceof MultipartHttpServletRequest) {
MultipartHttpServletRequest multipartReq = (MultipartHttpServletRequest) req;
Map<String, MultipartFile> files = multipartReq.getFileMap();
for (String formKey : files.keySet()) {
MultipartFile file = multipartReq.getFile(formKey);
//后缀名文件类型
String filename = file.getOriginalFilename();
//String suffixType = FileUtil.extName(filename);
String suffixType = StringUtils.substringAfterLast(filename, ".");
//文件头文件类型
String headerType = FileTypeUtil.getType(file.getInputStream());
//文件头与文件名后缀不匹配
if (!StringUtils.equalsIgnoreCase(suffixType, headerType)) {
String info = String.format("文件头与文件名后缀不匹配。文件名:%s,文件头:%s。", filename, headerType);
log.error(info);
resp.setStatus(HttpServletResponse.SC_OK);
resp.getWriter().write(info);
resp.flushBuffer();
return false;
}
}
}
return true;
}
}
# Resource接口
在日常程序开发中,处理外部资源是很繁琐的事情,我们可能需要处理URL资源、File资源资源、ClassPath相关资源、服务器相关资源(JBoss AS 5.x上的VFS资源)等等很多资源。因此处理这些资源需要使用不同的接口,这就增加了我们系统的复杂性;而且处理这些资源步骤都是类似的(打开资源、读取资源、关闭资源),因此如果能抽象出一个统一的接口来对这些底层资源进行统一访问,是不是很方便,而且使我们系统更加简洁,都是对不同的底层资源使用同一个接口进行访问。
Spring 提供一个Resource接口来统一这些底层资源一致的访问,而且提供了一些便利的接口,从而能提供我们的生产力。
# Resource接口
Spring的Resource接口代表底层外部资源,提供了对底层外部资源的一致性访问接口
public interface InputStreamSource {
//每次调用都将返回一个新鲜的资源对应的java.io.InputStream字节流
//调用者在使用完毕后必须关闭该资源
InputStream getInputStream() throws IOException;
}
//Resource接口继承InputStreamSource接口,并提供一些便利方法
public interface Resource extends InputStreamSource {
//返回当前Resource代表的底层资源是否存在,true表示存在
boolean exists();
//返回当前Resource代表的底层资源是否可读,true表示可读
boolean isReadable();
//返回当前Resource代表的底层资源是否已经打开
//如果返回true,则只能被读取一次然后关闭以避免资源泄露
//常见的Resource实现一般返回false
boolean isOpen();
//如果当前Resource代表的底层资源能由java.util.URL代表,则返回该URL
//否则抛出IOException
URL getURL() throws IOException;
//如果当前Resource代表的底层资源能由java.util.URI代表,则返回该URI
//否则抛出IOException
URI getURI() throws IOException;
//如果当前Resource代表的底层资源能由java.io.File代表,则返回该File
//否则抛出IOException
File getFile() throws IOException;
//返回当前Resource代表的底层文件资源的长度,一般是值代表的文件资源的长度
long contentLength() throws IOException;
//返回当前Resource代表的底层资源的最后修改时间
long lastModified() throws IOException;
//用于创建相对于当前Resource代表的底层资源的资源,比如当前Resource代表文件资源
//"d:/test/"则createRelative("test.txt")将返回文件资源"d:/test/test.txt"Resource资源
Resource createRelative(String relativePath) throws IOException;
//返回当前Resource代表的底层文件资源的文件路径
//比如File资源"file://d:/test.txt"将返回"d:/test.txt"
//而URL资源http://www.baidu.com将返回"",因为只返回文件路径
String getFilename();
//返回当前Resource代表的底层资源的描述符,通常就是资源的全路径
String getDescription();
}
# 内置Resource实现
Resource接口提供了很多内置Resource实现:
- ByteArrayResource:代表byte[]数组资源,对于getInputStream操作将返回一个ByteArrayInputStream
public void testByteArrayResource() {
Resource resource = new ByteArrayResource("Hello World!".getBytes());
if(resource.exists()) {
dumpStream(resource);
}
}
private void dumpStream(Resource resource) {
InputStream is = null;
try {
//1.获取文件资源
is = resource.getInputStream();
//2.读取资源
byte[] descBytes = new byte[is.available()];
is.read(descBytes);
System.out.println(new String(descBytes));
} catch (IOException e) {
e.printStackTrace();
}
finally {
try {
//3.关闭资源
is.close();
} catch (IOException e) {
}
}
}
注意
ByteArrayResource可多次读取数组资源,即isOpen()永远返回false
- InputStreamResource:代表java.io.InputStream字节流,对于getInputStream操作将直接返回该字节流
public void testInputStreamResource() {
ByteArrayInputStream bis = new ByteArrayInputStream("Hello World!".getBytes());
Resource resource = new InputStreamResource(bis);
if (resource.exists()) {
dumpStream(resource);
}
System.out.println(resource.isOpen());
}
注意
InputStreamResource只能读取一次该字节流,即isOpen()永远返回true
- FileSystemResource:代表java.io.File资源,对于getInputStream操作将返回底层文件的字节流
public void testFileResource() {
File file = new File("d:/test.txt");
Resource resource = new FileSystemResource(file);
if(resource.exists()) {
dumpStream(resource);
}
Assert.assertEquals(false, resource.isOpen());
}
注意
FileSystemResource可多次读取数组资源,即isOpen()永远返回false
- UrlResource:代表URL资源,用于简化URL资源访问
//http:通过标准的http协议访问web资源,如new UrlResource("http://地址");
//ftp:通过ftp协议访问资源,如new UrlResource("ftp://地址");
//file:通过file协议访问本地文件系统资源,如new UrlResource("file:d:/test.txt");
public void testUrlResource() throws IOException {
Resource resource = new UrlResource("file:d:/test.txt");
if (resource.exists()) {
dumpStream(resource);
}
System.out.println("path:" + resource.getURL().getPath());
Assert.assertEquals(false, resource.isOpen());
Resource resource2 = new UrlResource("http://www.baidu.com");
if (resource2.exists()) {
dumpStream(resource2);
}
System.out.println("path:" + resource2.getURL().getPath());
Assert.assertEquals(false, resource2.isOpen());
}
注意
UrlResource可多次读取数组资源,即isOpen()永远返回false
- ClassPathResource:代表classpath路径的资源,将使用ClassLoader进行加载资源
//ClassPathResource提供了三个构造器:
//使用默认的ClassLoader加载path类路径资源;
public ClassPathResource(String path):
//使用指定的ClassLoader加载path类路径资源;
public ClassPathResource(String path, ClassLoader classLoader):
//使用指定的类加载path类路径资源,将加载相对于当前类的路径的资源;
public ClassPathResource(String path, Class<?> clazz):
/**
* 使用默认的加载器加载资源,将加载当前ClassLoader类路径上相对于根路径的资源
* @throws IOException
*/
@Test
public void testClasspathResourceByDefaultClassLoader() throws IOException {
Resource resource = new ClassPathResource("test1.properties");
if (resource.exists()) {
dumpStream(resource);
}
System.out.println("path:" + resource.getFile().getAbsolutePath());
Assert.assertEquals(false, resource.isOpen());
}
/**
* 使用指定的ClassLoader进行加载资源,将加载指定的ClassLoader类路径上相对于根路径
* 的资源
* @throws IOException
*/
@Test
public void testClasspathResourceByClassLoader() throws IOException {
//ClassLoader loader = Thread.currentThread().getContextClassLoader();
//System.out.println(loader.getResource("").getPath());
ClassLoader cl = this.getClass().getClassLoader();
Resource resource = new ClassPathResource("test1.properties", cl);
if (resource.exists()) {
dumpStream(resource);
}
System.out.println("path:" + resource.getFile().getAbsolutePath());
Assert.assertEquals(false, resource.isOpen());
}
/**
* 使用指定的类进行加载资源,将尝试加载相对于当前类的路径的资源
* @throws IOException
*/
@Test
public void testClasspathResourceByClass() throws IOException {
Class clazz = this.getClass();
Resource resource1 = new ClassPathResource("/test1.properties", clazz);
if (resource1.exists()) {
dumpStream(resource1);
}
System.out.println("path:" + resource1.getFile().getAbsolutePath());
Assert.assertEquals(false, resource1.isOpen());
Resource resource2 = new ClassPathResource("/test1.properties", this.getClass());
if (resource2.exists()) {
dumpStream(resource2);
}
System.out.println("path:" + resource2.getFile().getAbsolutePath());
Assert.assertEquals(false, resource2.isOpen());
}
/**
* 加载jar包里的资源,首先在当前类路径下找不到,最后才到Jar包里找,而且在第一个Jar
* 包里找到的将被返回
* @throws IOException
*/
@Test
public void testClasspathResourceFromJar() throws IOException {
Resource resource = new ClassPathResource("overview.html");
if (resource.exists()) {
dumpStream(resource);
}
System.out.println("path:" + resource.getURL().getPath());
Assert.assertEquals(false, resource.isOpen());
}
# ServerHttpRequest
ServerHttpRequest和HttpServletRequest的区别
HttpServletRequest 是tomcat提供的,ServerHttpRequest 是 spring框架提供的
ServerHttpRequest接口的实现类ServletServerHttpRequest,可通过方法getServletRequest()获取HttpServletRequest
if(exchange.getRequest() instanceof ServletServerHttpRequest) {
ServletServerHttpRequest request = (ServletServerHttpRequest) exchange.getRequest();
HttpServletRequest httpServletRequest = request.getServletRequest();
}
ServerHttpRequest的主要适用场景:
- 在使用springboot的websocket时,获取url中的参数
public class HandShake extends HttpSessionHandshakeInterceptor {
/**
* 握手前参数和权限设置与校验
*/
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler,
Map<String, Object> attributes) {
// 初始化Session信息
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
// 获取参数 必填校验
String xxx = servletRequest.getServletRequest().getParameter(xxx);
}
}
- 在网关接口中获取相关request
@PostMapping("/captch")
public Mono<Result> getCaptch(ServerWebExchange exchange){
ServerHttpRequest request = exchange.getRequest();
}
- webflux使用ServerHttpRequest 获取多body体内容
@RequestMapping(value="/testBodys",method=RequestMethod.GET)
@ResponseBody
public Mono<ResponseEntity> testHttpRequest(ServerHttpRequest httpRequest){
return Mono.justOrEmpty(new ResponseEntity(HttpStatus.OK));
}
# Jar包加密
# 采用classfinal-maven-plugin插件
直接配置一个插件就可以实现源码的安全性保护。并且可以对yml、properties配置文件以及lib目录下的maven依赖进行加密处理。若想指定机器启动,支持绑定机器,项目加密后只能在特定机器运行
# 功能特点
- 加密后,方法体被清空,保留方法参数、注解等信息.主要兼容swagger文档注解扫描
- 方法体被清空后,反编译只能看到方法名和注解,看不到方法体的具体内容
- 加密后的项目需要设置javaagent来启动,启动过程中解密class,完全内存解密,不留下任何解密后的文件
- 启动加密后的jar,生成xxx-encrypted.jar,这个就是加密后的jar文件,加密后不可直接执行
# 无密码启动方式
java -javaagent:xxx-encrypted.jar -jar xxx-encrypted.jar
# 有密码启动方式
java -javaagent:xxx-encrypted.jar='-pwd= 密码' -jar xxx-encrypted.jar
<!-- 在启动类的pom.xml文件中加如下插件-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>net.roseboy</groupId>
<artifactId>classfinal-maven-plugin</artifactId>
<version>1.2.1</version>
<configuration>
<!-- #表示启动时不需要密码,只是一个启动密码,对于代码混淆来说没什么用 -->
<password>#</password>
<excludes>org.spring</excludes>
<!-- 加密的包名,多个包用逗号分开 -->
<packages>${groupId}</packages>
<!-- 加密的配置文件,多个包用逗号分开 -->
<cfgfiles>application.yml,application-dev.yml</cfgfiles>
<!-- jar包lib下面要加密的jar依赖文件,多个包用逗号分开 -->
<libjars>hutool-all.jar</libjars>
<!-- 指定机器启动,机器码 -->
<code>xxxx</code>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>classFinal</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
注意
该插件时要放到spring-boot-maven-plugin插件后面,否则不起作用
# 如何绑定机器启动
下载到classfinal-fatjar-1.2.1.jar依赖,在当前依赖下cmd执行java -jar classfinal-fatjar-1.2.1.jar -C命令,会自动生成一串机器码
将此生成好的机器码,放到maven插件中的code里面即可
# 实现数据库配置文件的加密
在SpringBoot中我们可以通过Jasypt来进行数据库配置信息的加密操作,Jasypt是一个用于加密和解密敏感数据的Java 库,可以与Spring Boot实现无缝的集成
- 在项目中引入Jasypt Starter的依赖配置,如下所示
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
- 在本地application-dev.yml文件中添加Jasypt配置
jasypt:
encryptor:
# 盐值
password: 123
# 指定加密方式
algorithm: PBEWithMD5AndDES
iv-generator-classname: org.jasypt.iv.NoIvGenerator
property:
# 标识为加密属性的前缀
prefix: ENC(
# 标识为加密属性的后缀
suffix: )
注意
这里是解密配置,加密时的算法和盐值必须与以上配置中的algorithm与password(盐值)一致
- 编写JasyptUtil工具类用于加密/解密
public class JasyptUtil {
/**
* PBE 算法
*/
public static final String PBE_ALGORITHMS_MD5_DES = "PBEWITHMD5ANDDES";
public static final String PBE_ALGORITHMS_MD5_TRIPLEDES = "PBEWITHMD5ANDTRIPLEDES";
public static final String PBE_ALGORITHMS_SHA1_DESEDE = "PBEWITHSHA1ANDDESEDE";
public static final String PBE_ALGORITHMS_SHA1_RC2_40 = "PBEWITHSHA1ANDRC2_40";
private JasyptUtil() {
}
/**
* Jasypt 加密
*
* @param encryptedStr 加密字符串
* @param password 盐值
* @return
*/
public static String encrypt(String encryptedStr, String password) {
return encrypt(encryptedStr, PBE_ALGORITHMS_MD5_DES, password);
}
/**
* Jasypt 加密
*
* @param encryptedStr 加密字符串
* @param algorithm 加密算法
* PBE ALGORITHMS: [PBEWITHMD5ANDDES,
* PBEWITHMD5ANDTRIPLEDES,
* PBEWITHSHA1ANDDESEDE,
* PBEWITHSHA1ANDRC2_40]
* @param password 盐值
* @return
*/
public static String encrypt(String encryptedStr, String algorithm, String password) {
// StandardPBEStringEncryptor
// StandardPBEBigDecimalEncryptor
// StandardPBEBigIntegerEncryptor
// StandardPBEByteEncryptor
StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
EnvironmentPBEConfig config = new EnvironmentPBEConfig();
// 指定加密算法
config.setAlgorithm(algorithm);
// 加密盐值
config.setPassword(password);
//config.setIvGeneratorClassName("org.jasypt.iv.NoIvGenerator");
encryptor.setConfig(config);
// 加密
return encryptor.encrypt(encryptedStr);
}
/**
* Jasypt 解密
*
* @param decryptStr 解密字符串
* @param password 盐值
* @return
*/
public static String decrypt(String decryptStr, String password) {
return decrypt(decryptStr, PBE_ALGORITHMS_MD5_DES, password);
}
/**
* Jasypt 解密
*
* @param decryptStr 解密字符串
* @param algorithm 指定解密算法:解密算法要与加密算法一一对应
* PBE ALGORITHMS: [PBEWITHMD5ANDDES,
* PBEWITHMD5ANDTRIPLEDES,
* PBEWITHSHA1ANDDESEDE,
* PBEWITHSHA1ANDRC2_40]
* @param password 盐值
* @return
*/
public static String decrypt(String decryptStr, String algorithm, String password) {
// StandardPBEStringEncryptor
// StandardPBEBigDecimalEncryptor
// StandardPBEBigIntegerEncryptor
// StandardPBEByteEncryptor
StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
EnvironmentPBEConfig config = new EnvironmentPBEConfig();
// 指定解密算法:解密算法要与加密算法一一对应
config.setAlgorithm(algorithm);
// 加密秘钥
config.setPassword(password);
//config.setIvGeneratorClassName("org.jasypt.iv.NoIvGenerator");
encryptor.setConfig(config);
// 解密
return encryptor.decrypt(decryptStr);
}
public static void main(String[] args) {
String encryptedStr = "I am the string to be encrypted";
String algorithm = PBE_ALGORITHMS_SHA1_RC2_40;
String password = "salt";
String str = JasyptUtil.encrypt(encryptedStr, algorithm, password);
System.out.println("加密后的字符串:" + str);
System.out.println("解密后的字符串:" + JasyptUtil.decrypt(str,algorithm,password));
}
}
- 编写测试类进行测试jasypt加密测试
package com.agileboot.admin.jasypt;
import com.agileboot.common.utils.jasypt.JasyptUtil;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class jasyptTest {
/**
* PBE 算法
*/
public static final String PBE_ALGORITHMS_MD5_DES = "PBEWITHMD5ANDDES";
public static final String PBE_ALGORITHMS_MD5_TRIPLEDES = "PBEWITHMD5ANDTRIPLEDES";
public static final String PBE_ALGORITHMS_SHA1_DESEDE = "PBEWITHSHA1ANDDESEDE";
public static final String PBE_ALGORITHMS_SHA1_RC2_40 = "PBEWITHSHA1ANDRC2_40";
@Test
public void TsetJasypt() {
String encryptedStr = "root";
String algorithm = PBE_ALGORITHMS_MD5_DES;
String password = "salt";
String str = JasyptUtil.encrypt(encryptedStr, algorithm, password);
System.out.println("加密后的字符串:" + str);
System.out.println("解密后的字符串:" + JasyptUtil.decrypt(str,algorithm,password));
}
}
- 在Spring Boot的配置文件中配置加密属性,ENC()的内容就是加密后的内容
spring.datasource.username=ENC(encryptedUsername)
spring.datasource.password=ENC(encryptedPassword)
- 启动类加上@EnableEncryptableProperties这个注解,用于自动解密
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
@EnableEncryptableProperties
public class RuoYiApplication
{
public static void main(String[] args)
{
SpringApplication.run(RuoYiApplication.class, args);
}
}
- Jasypt 会在运行时自动解密 ENC() 中的内容,所以不需要额外的代码就可以访问解密后的值
@RestController
public class TestController {
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@GetMapping("/db-credentials")
public String getCredentials() {
return "Username: " + username + ", Password: " + password;
}
}
# 文件上传之秒传/断点续传/分片上传
# 秒传
# 简单来说,当你尝试上传某个文件时,服务器会首先进行MD5校验
# 如果服务器上已经存在与该文件MD5值相同的文件,服务器就会直接给你一个新地址
# 这样你下载或访问的其实都是服务器上已有的那个文件,这个过程就被称为"秒传"
# 想要避免秒传,关键在于改变文件的MD5值,因为MD5值是文件内容的唯一标识
# 仅仅改变文件名是不足以改变MD5值的,需要对文件进行实质性的修改,比如在一个文本文件中添加几个字
# 这样文件的MD5值就会改变,上传时就不会被识别为重复文件,从而实现非秒传上传
- 前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>秒传文件上传</title>
</head>
<body>
<input type="file" id="fileInput" />
<button onclick="uploadFile()">上传文件</button>
<script>
async function uploadFile() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert('请选择文件');
return;
}
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
alert('文件上传成功');
} else {
alert('文件上传失败');
}
} catch (error) {
console.error('文件上传失败:', error);
}
}
</script>
</body>
</html>
- 后端代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
@SpringBootApplication
public class FileUploadApplication {
public static void main(String[] args) {
SpringApplication.run(FileUploadApplication.class, args);
}
}
@RestController
public class FileUploadController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@PostMapping("/upload")
public String uploadFile(@RequestParam("file") MultipartFile file) {
try {
Path path = Paths.get("uploads/" + file.getOriginalFilename());
if (!Files.exists(path.getParent())) {
Files.createDirectories(path.getParent());
}
file.transferTo(path.toFile());
String fileMd5 = getFileMd5(file.getInputStream());
String key = "file_" + fileMd5;
if (redisTemplate.opsForValue().get(key) != null) {
return "秒传成功";
} else {
redisTemplate.opsForValue().set(key, "false");
redisTemplate.opsForValue().set("block_" + fileMd5, path.toString());
return "文件正在上传中";
}
} catch (IOException | NoSuchAlgorithmException e) {
return "文件上传失败";
}
}
private String getFileMd5(InputStream inputStream) throws NoSuchAlgorithmException,
IOException {
MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] hash = digest.digest(inputStream.readAllBytes());
return String.format("%032x", new BigInteger(1, hash));
}
}
- 配置文件
spring.servlet.multipart.max-file-size=2MB
spring.servlet.multipart.max-request-size=10MB
# Redis配置
spring.redis.host=localhost
spring.redis.port=6379
# 分片上传
# 分片上传是一种高效处理大文件上传的方法,其核心是将大文件按照预设的大小切割成多个较小的数据分片
# 在上传过程中,前端会记录分片的总数以及当前正在上传的分片编号,并通知后端相应的信息
# 待所有分片均成功上传至服务器后,后端将负责将这些分散的分片重新组合拼接,还原成原始的完整文件
# 这样,即便是面对体积庞大的文件,也能通过分片的方式实现顺畅且稳定的上传过程
# 注:大文件的分片是在前端做的,不是后端!
- 前端代码
# 前端上传代码计算文件MD5值用了spark-md5这个库,使用也是比较简单的
# 为什么要计算MD5,因为文件在传输写入过程中可能会出现错误,导致最终合成的文件可能和原文件不一样
# 所以要对比一下前端计算的MD5和后端计算的MD5是不是一样,保证上传数据的一致性
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>分片上传</title>
<script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
</head>
<body>
<form enctype="multipart/form-data">
<input type="file" name="fileInput" id="fileInput">
<input type="button" value="计算文件MD5" onclick="calculateFileMD5()">
<input type="button" value="上传" onclick="uploadFile()">
<input type="button" value="检测文件完整性" onclick="checkFile()">
</form>
<p>
文件MD5:
<span id="fileMd5"></span>
</p>
<p>
上传结果:
<span id="uploadResult"></span>
</p>
<p>
检测文件完整性:
<span id="checkFileRes"></span>
</p>
<script>
//每片的大小
var chunkSize = 1 * 1024 * 1024;
var uploadResult = document.getElementById("uploadResult")
var fileMd5Span = document.getElementById("fileMd5")
var checkFileRes = document.getElementById("checkFileRes")
var fileMd5;
function calculateFileMD5(){
var fileInput = document.getElementById('fileInput');
var file = fileInput.files[0];
getFileMd5(file).then((md5) => {
console.info(md5)
fileMd5=md5;
fileMd5Span.innerHTML=md5;
})
}
function uploadFile() {
var fileInput = document.getElementById('fileInput');
var file = fileInput.files[0];
if (!file) return;
if (!fileMd5) return;
//获取到文件
let fileArr = this.sliceFile(file);
//保存文件名称
let fileName = file.name;
fileArr.forEach((e, i) => {
//创建formdata对象
let data = new FormData();
data.append("totalNumber", fileArr.length)
data.append("chunkSize", chunkSize)
data.append("chunkNumber", i)
data.append("md5", fileMd5)
data.append("file", new File([e],fileName));
upload(data);
})
}
/**
* 计算文件md5值
*/
function getFileMd5(file) {
return new Promise((resolve, reject) => {
let fileReader = new FileReader()
fileReader.onload = function (event) {
let fileMd5 = SparkMD5.ArrayBuffer.hash(event.target.result)
resolve(fileMd5)
}
fileReader.readAsArrayBuffer(file)
})
}
function upload(data) {
var xhr = new XMLHttpRequest();
// 当上传完成时调用
xhr.onload = function () {
if (xhr.status === 200) {
uploadResult.append( '上传成功分片:' +data.get("chunkNumber")+'\t' ) ;
}
}
xhr.onerror = function () {
uploadResult.innerHTML = '上传失败';
}
// 发送请求
xhr.open('POST', '/uploadBig', true);
xhr.send(data);
}
function checkFile() {
var xhr = new XMLHttpRequest();
// 当上传完成时调用
xhr.onload = function () {
if (xhr.status === 200) {
checkFileRes.innerHTML = '检测文件完整性成功:' + xhr.responseText;
}
}
xhr.onerror = function () {
checkFileRes.innerHTML = '检测文件完整性失败';
}
// 发送请求
xhr.open('POST', '/checkFile', true);
let data = new FormData();
data.append("md5", fileMd5)
xhr.send(data);
}
function sliceFile(file) {
const chunks = [];
let start = 0;
let end;
while (start < file.size) {
end = Math.min(start + chunkSize, file.size);
chunks.push(file.slice(start, end));
start = end;
}
return chunks;
}
</script>
</body>
</html>
- 后端就两个接口/uploadBig用于每一片文件的上传和/checkFile检测文件的MD5
# 这里需要注意的:
# MD5.conf每一次检测文件不存在里创建个空文件,使用byte[] bytes = new byte[totalNumber]
# 将每一位状态设置为0,从0位天始,第N位表示第N个分片的上传状态,0-未上传 1-已上传
# 当每将上传成功后使用randomAccessConfFile.seek(chunkNumber)将对就设置为1
# randomAccessFile.seek(chunkNumber * chunkSize)
# 可以将光标移到文件指定位置开始写数据,每一个文件每将上传分片编号chunkNumber都是不一样的
# 所以各自写自己文件块,多线程写同一个文件不会出现线程安全问题
# 大文件写入时用RandomAccessFile可能比较慢,可以使用MappedByteBuffer内存映射来加速大文件写入
# 不过使用MappedByteBuffer如果要删除文件可能会删除不掉,因为删了磁盘上的文件,内存的文件还存在
@RestController
public class UploadController {
public static final String UPLOAD_PATH = "D:\\upload\\";
/**
* @param chunkSize 每个分片大小
* @param chunkNumber 当前分片
* @param md5 文件总MD5
* @param file 当前分片文件数据
* @return
* @throws IOException
*/
@RequestMapping("/uploadBig")
public ResponseEntity<Map<String, String>> uploadBig(@RequestParam Long chunkSize,
@RequestParam Integer totalNumber,
@RequestParam Long chunkNumber,
@RequestParam String md5,
@RequestParam MultipartFile file)
throws IOException {
//文件存放位置
String dstFile = String.format("%s\\%s\\%s.%s", UPLOAD_PATH, md5, md5,
StringUtils.getFilenameExtension(file.getOriginalFilename()));
//上传分片信息存放位置
String confFile = String.format("%s\\%s\\%s.conf", UPLOAD_PATH, md5, md5);
//第一次创建分片记录文件
//创建目录
File dir = new File(dstFile).getParentFile();
if (!dir.exists()) {
dir.mkdir();
//所有分片状态设置为0
byte[] bytes = new byte[totalNumber];
Files.write(Path.of(confFile), bytes);
}
//随机分片写入文件
try (RandomAccessFile randomAccessFile = new RandomAccessFile(dstFile, "rw");
RandomAccessFile randomAccessConfFile = new RandomAccessFile(confFile, "rw");
InputStream inputStream = file.getInputStream()) {
//定位到该分片的偏移量
randomAccessFile.seek(chunkNumber * chunkSize);
//写入该分片数据
randomAccessFile.write(inputStream.readAllBytes());
//定位到当前分片状态位置
randomAccessConfFile.seek(chunkNumber);
//设置当前分片上传状态为1
randomAccessConfFile.write(1);
}
return ResponseEntity.ok(Map.of("path", dstFile));
}
/**
* 获取文件分片状态,检测文件MD5合法性
*
* @param md5
* @return
* @throws Exception
*/
@RequestMapping("/checkFile")
public ResponseEntity<Map<String, String>> uploadBig(@RequestParam String md5)
throws Exception {
String uploadPath = String.format("%s\\%s\\%s.conf", UPLOAD_PATH, md5, md5);
Path path = Path.of(uploadPath);
//MD5目录不存在文件从未上传过
if (!Files.exists(path.getParent())) {
return ResponseEntity.ok(Map.of("msg", "文件未上传"));
}
//判断文件是否上传成功
StringBuilder stringBuilder = new StringBuilder();
byte[] bytes = Files.readAllBytes(path);
for (byte b : bytes) {
stringBuilder.append(String.valueOf(b));
}
//所有分片上传完成计算文件MD5
if (!stringBuilder.toString().contains("0")) {
File file = new File(String.format("%s\\%s\\", UPLOAD_PATH, md5));
File[] files = file.listFiles();
String filePath = "";
for (File f : files) {
//计算文件MD5是否相等
if (!f.getName().contains("conf")) {
filePath = f.getAbsolutePath();
try (InputStream inputStream = new FileInputStream(f)) {
String md5pwd = DigestUtils.md5DigestAsHex(inputStream);
if (!md5pwd.equalsIgnoreCase(md5)) {
return ResponseEntity.ok(Map.of("msg", "文件上传失败"));
}
}
}
}
return ResponseEntity.ok(Map.of("path", filePath));
} else {
//文件未上传完成,反回每个分片状态,前端将未上传的分片继续上传
return ResponseEntity.ok(Map.of("chucks", stringBuilder.toString()));
}
}
}
# 断点续传
# 断点续传是一种先进的文件上传技术,专为解决大文件上传过程中可能遇到的中断问题而设计
# 该技术允许在上传流程被意外打断后,用户能够从中断的具体位置继续上传,而无需从头开始整个文件的传输
# 这一特性在上传大文件时尤为关键,因为它能够大幅度缩短上传所需的时间,显著提升用户体验和效率
# 实现断点续传的核心机制通常涉及一个特定的接口,如/checkFile
# 该接口的作用是在上传开始前或过程中被调用,以检查服务器上是否存在该文件的部分上传记录
# 如果文件中有尚未完成上传的分片(即之前上传过程中断留下的数据片段)
# checkFile接口会返回一个包含chunks值的响应,其中对应位置的分片如果尚未上传完成,其值将被标记为0
# 前端接收到这个响应后,会智能地识别出哪些分片是已经上传成功的,哪些是需要继续上传的
# 并仅针对那些未完成的分片发起上传请求。一旦所有未完成的分片都被成功上传
# 前端会再次调用/checkFile接口进行最终的检查,确认所有分片都已完整上传,从而完成整个断点续传的过程
- 前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>分片上传</title>
<script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
</head>
<body>
分片上传
<form enctype="multipart/form-data">
<input type="file" name="fileInput" id="fileInput">
<input type="button" value="计算文件MD5" onclick="calculateFileMD5()">
<input type="button" value="上传" onclick="uploadFile()">
<input type="button" value="检测文件完整性" onclick="checkFile()">
</form>
<p>
文件MD5:
<span id="fileMd5"></span>
</p>
<p>
上传结果:
<span id="uploadResult"></span>
</p>
<p>
检测文件完整性:
<span id="checkFileRes"></span>
</p>
<script>
//每片的大小
var chunkSize = 1 * 1024 * 1024;
var uploadResult = document.getElementById("uploadResult")
var fileMd5Span = document.getElementById("fileMd5")
var checkFileRes = document.getElementById("checkFileRes")
var fileMd5;
function calculateFileMD5(){
var fileInput = document.getElementById('fileInput');
var file = fileInput.files[0];
getFileMd5(file).then((md5) => {
console.info(md5)
fileMd5=md5;
fileMd5Span.innerHTML=md5;
})
}
function uploadFile() {
var fileInput = document.getElementById('fileInput');
var file = fileInput.files[0];
if (!file) return;
if (!fileMd5) return;
//获取到文件
let fileArr = this.sliceFile(file);
//保存文件名称
let fileName = file.name;
fileArr.forEach((e, i) => {
//创建formdata对象
let data = new FormData();
data.append("totalNumber", fileArr.length)
data.append("chunkSize", chunkSize)
data.append("chunkNumber", i)
data.append("md5", fileMd5)
data.append("file", new File([e],fileName));
upload(data);
})
}
/**
* 计算文件md5值
*/
function getFileMd5(file) {
return new Promise((resolve, reject) => {
let fileReader = new FileReader()
fileReader.onload = function (event) {
let fileMd5 = SparkMD5.ArrayBuffer.hash(event.target.result)
resolve(fileMd5)
}
fileReader.readAsArrayBuffer(file)
})
}
function upload(data) {
var xhr = new XMLHttpRequest();
// 当上传完成时调用
xhr.onload = function () {
if (xhr.status === 200) {
uploadResult.append( '上传成功分片:' +data.get("chunkNumber")+'\t' ) ;
}
}
xhr.onerror = function () {
uploadResult.innerHTML = '上传失败';
}
// 发送请求
xhr.open('POST', '/uploadBig', true);
xhr.send(data);
}
function checkFile() {
var xhr = new XMLHttpRequest();
// 当上传完成时调用
xhr.onload = function () {
if (xhr.status === 200) {
checkFileRes.innerHTML = '检测文件完整性成功:' + xhr.responseText;
}
}
xhr.onerror = function () {
checkFileRes.innerHTML = '检测文件完整性失败';
}
// 发送请求
xhr.open('POST', '/checkFile', true);
let data = new FormData();
data.append("md5", fileMd5)
xhr.send(data);
}
function sliceFile(file) {
const chunks = [];
let start = 0;
let end;
while (start < file.size) {
end = Math.min(start + chunkSize, file.size);
chunks.push(file.slice(start, end));
start = end;
}
return chunks;
}
// 其他代码保持不变...
function checkFileAndContinue() {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/checkFile', true);
let data = new FormData();
data.append("md5", fileMd5);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
if (response.msg === '文件未上传') {
// 文件未上传,开始上传所有分片
uploadFile();
} else if (response.msg === '文件上传失败') {
uploadResult.innerHTML = '文件MD5校验失败';
} else {
// 有分片未上传,继续上传未完成的分片
const chunksStatus = response.chucks;
let chunksToUpload = [];
for (let i = 0; i < chunksStatus.length; i++) {
if (chunksStatus.charAt(i) === '0') {
chunksToUpload.push(fileArr[i]);
}
}
if (chunksToUpload.length > 0) {
uploadChunks(chunksToUpload);
} else {
uploadResult.innerHTML = '所有分片已上传';
}
}
} else {
uploadResult.innerHTML = '检查文件状态失败';
}
}
};
xhr.send(data);
}
function uploadChunks(chunksToUpload) {
chunksToUpload.forEach((chunk, i) => {
let data = new FormData();
data.append("totalNumber", fileArr.length);
data.append("chunkSize", chunkSize);
data.append("chunkNumber", i);
data.append("md5", fileMd5);
data.append("file", new File([chunk], fileName));
upload(data);
});
}
// 在用户选择文件后,先计算MD5并检查文件上传状态
document.getElementById('fileInput').addEventListener('change', function() {
var fileInput = document.getElementById('fileInput');
var file = fileInput.files[0];
if (file) {
calculateFileMD5(file).then(md5 => {
fileMd5 = md5;
fileMd5Span.innerHTML = md5;
checkFileAndContinue(); // 检查文件上传状态
});
}
});
</script>
</body>
</html>
- 后端代码
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.util.HashMap;
import java.util.Map;
@RestController
public class UploadController {
public static final String UPLOAD_PATH = "D:\\upload\\";
/**
* 上传分片文件
*
* @param chunkSize 每个分片大小
* @param totalNumber 总分片数
* @param chunkNumber 当前分片编号
* @param md5 文件MD5
* @param file 当前分片文件数据
* @return
* @throws IOException
*/
@PostMapping("/uploadBig")
public ResponseEntity<Map<String, String>> uploadBig(
@RequestParam("chunkSize") Long chunkSize,
@RequestParam("totalNumber") Integer totalNumber,
@RequestParam("chunkNumber") Integer chunkNumber,
@RequestParam("md5") String md5,
@RequestParam("file") MultipartFile file) throws IOException {
// 确保文件目录存在
String dirPath = String.format("%s%s\\", UPLOAD_PATH, md5);
File dir = new File(dirPath);
if (!dir.exists()) {
dir.mkdirs();
}
// 文件存放位置
String fileName = String.format("%s.part", md5);
String dstFilePath = String.format("%s%s\\%s", UPLOAD_PATH, md5, fileName);
// 分片信息存放位置
String confFilePath = String.format("%s%s\\%s.conf", UPLOAD_PATH, md5, md5);
// 写入分片数据
try (FileOutputStream fos = new FileOutputStream(dstFilePath, true);
FileChannel fileChannel = fos.getChannel();
InputStream is = file.getInputStream()) {
long offset = (long) chunkNumber * chunkSize;
fileChannel.position(offset);
fileChannel.transferFrom(is.getChannel(), offset, file.getSize());
}
// 更新分片状态
byte[] statusArray = new byte[totalNumber];
int index = chunkNumber;
if (!Files.exists(Paths.get(confFilePath))) {
try (FileOutputStream confFos = new FileOutputStream(confFilePath)) {
confFos.write(statusArray);
}
}
statusArray[index] = 1;
try (RandomAccessFile randomAccessFile = new RandomAccessFile(confFilePath, "rw")){
randomAccessFile.seek(index);
randomAccessFile.write(statusArray[index]);
}
Map<String, String> response = new HashMap<>();
response.put("path", dstFilePath);
return ResponseEntity.ok(response);
}
/**
* 检查文件分片状态,检测文件MD5合法性
*
* @param md5 文件MD5
* @return
* @throws IOException
*/
@PostMapping("/checkFile")
public ResponseEntity<Map<String, Object>> checkFile(@RequestParam("md5") String md5)
throws IOException {
String confFilePath = String.format("%s%s\\%s.conf", UPLOAD_PATH, md5, md5);
Path confPath = Paths.get(confFilePath);
if (!Files.exists(confPath.getParent())) {
return ResponseEntity.ok(Map.of("msg", "文件未上传"));
}
boolean isComplete = true;
try (RandomAccessFile accessFile = new RandomAccessFile(confFilePath, "r")) {
for (int i = 0; i < accessFile.length(); i++) {
if (accessFile.readByte() == 0) {
isComplete = false;
break;
}
}
}
if (isComplete) {
// 计算合并后的文件MD5
String finalFilePath = String.format("%s%s\\%s", UPLOAD_PATH, md5, md5);
try (InputStream is = new FileInputStream(finalFilePath)) {
String calculatedMd5 = DigestUtils.md5Hex(IOUtils.toByteArray(is));
return ResponseEntity.ok(Map.of("msg", "文件上传成功", "md5",calculatedMd5));
} catch (IOException e) {
return ResponseEntity.ok(Map.of("msg", "文件合并失败"));
}
} else {
// 未完成上传,返回未上传的分片信息
try (RandomAccessFile accessFile = new RandomAccessFile(confFilePath, "r")) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < accessFile.length(); i++) {
stringBuilder.append(accessFile.readByte() == 0 ? "0" : "1");
}
return ResponseEntity.ok(Map.of("msg", "文件未上传完成", "chunks",
stringBuilder.toString()));
}
}
}
}
# 解决接口防抖
防抖(Debouncing)是一种控制事件频繁触发的技术,用于限定某个事件在固定的时间内触发的频率
# 前端防抖
我们可以通过JavaScript中的setTimeout 和 clearTimeout 来实现防抖
let debounceTimeout;
const debounceDelay = 500; // 500ms 防抖
function handleSearch(event) {
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(function() {
fetch(`/search?query=${event.target.value}`)
.then(response => response.json())
.then(data => console.log(data));
}, debounceDelay);
}
# 基于Redis的防抖
- 在pom.xml文件中引入Redis相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 接下来就是配置Redis的连接
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=your_password
- 创建一个服务类,用来实现防抖逻辑,检查每个请求的时间是否过于接近,避免出现重复请求的情况
@Service
public class DebounceService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String DEBOUNCE_KEY = "debounce_key:"; // 这个key用于标识每个请求
// 防抖时间,单位:秒
private static final long DEBOUNCE_TIME = 2;
public boolean isDebounced(String userId) {
String key = DEBOUNCE_KEY + userId;
// 获取上一次请求的时间戳
String lastRequestTime = redisTemplate.opsForValue().get(key);
long currentTime = System.currentTimeMillis();
if (lastRequestTime != null) {
// 如果距离上次请求时间小于防抖时间,认为是重复请求
long elapsed = currentTime - Long.parseLong(lastRequestTime);
if (elapsed < DEBOUNCE_TIME * 1000) {
return true; // 需要防抖
}
}
// 更新请求时间戳
redisTemplate.opsForValue().set(key, String.valueOf(currentTime), DEBOUNCE_TIME, TimeUnit.SECONDS);
return false;
}
}
- 在控制器中通过上面的服务来实现接口请求评率的判断,从而实现接口的防抖操作
@RestController
public class SearchController {
@Autowired
private DebounceService debounceService;
@GetMapping("/search")
public String search(@RequestParam String query, @RequestParam String userId) {
if (debounceService.isDebounced(userId)) {
return "Please wait before making another request.";
}
// 执行搜索操作
return "Searching for: " + query;
}
}
# 基于线程池的防抖
- 在 Spring Boot 中通过 @Configuration 注解配置类来配置一个线程池操作
@Configuration
public class ThreadPoolConfig {
@Bean
public Executor taskExecutor() {
return Executors.newSingleThreadExecutor();
}
}
- 接下来,利用这个线程池来实现接口的防抖操作
@Service
public class DebounceService {
@Async
public void debounceTask(Runnable task, long delay) {
try {
Thread.sleep(delay);
task.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
- 在Controller控制器中通过创建异步任务来对接口实现防抖逻辑的处理
@RestController
public class SearchController {
@Autowired
private DebounceService debounceService;
@GetMapping("/search")
public String search(@RequestParam String query) {
Runnable task = () -> {
// 执行搜索操作
System.out.println("Searching for: " + query);
};
// 延迟1秒执行任务,模拟防抖效果
debounceService.debounceTask(task, 1000);
return "Request received. Please wait for search to complete.";
}
}
# 证书管理引擎 TrueLicense
License也就是版权许可证书,一般用于收费软件给付费用户提供的访问许可证明,使用方法 (opens new window)
- 生成密钥对,使用Keytool生成公私钥证书库
- 授权者保留私钥,使用私钥和使用日期生成证书license
- 公钥与生成的证书给使用者(放在验证的代码中使用),验证证书license是否在有效期内