项目简介
业务描述
基于Spring Cloud Alibaba解决方案实现文件上传
初始架构设计
前后端分离架构,服务设计基于spring cloud alibaba解决方案
初始架构设计,后续增加网关工程,认证工程
工程创建及初始化
工程结构
创建父工程
创建父工程
创建文件服务工程
处理文件上传
创建客户端服务工程
用于定义一些静态页面,例如文件上传页面
父工程初始化
添加依赖版本管理
spring-boot-dependencies
spring-cloud-dependencies
spring-cloud-alibaba-dependencies
注意别忘记:
<type>pom</type>
<scope>import</scope>
同时添加依赖lombok并设置
<scope>provided</scope>
设置编译版本
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
或
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
文件资源服务实现
添加项目依赖
sca-resource中添加依赖
Spring Boot Web (服务-内置tomcat) spring-boot-starter-web
Nacos Discovery (服务注册发现) spring-cloud-starter-alibaba-nacos-discovery
Nacos Config (配置中心) spring-cloud-starter-alibaba-nacos-config
Sentinel (流量防卫兵-限流和熔断) spring-cloud-starter-alibaba-sentinel
Spring Boot 监控 spring-boot-starter-actuator
服务初始化配置
创建bootstrap.yml(后续要写到配置中心,所以bootstrap.yml)
server:
port: 8881
spring:
application:
name: sca-resource
resources: #定义可以访问到上传资源的路径,localhost:8881/app.png
static-locations: file:d:/uploads #静态资源路径(原先存储到resources/static目录下的资源可以存储到此目录中)
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yml
#这里的配置是自己定义的,后续会在一些相关类中通过@Value注解进行读取
jt:
resource:
path: d:/uploads #设计上传文件存储的根目录(后续要写到配置文件)
host: http://localhost:8881/ #定义上传文件对应的访问服务器
构建项目启动类
创建启动类。启动检测配置是否有误
Controller逻辑实现
定义 处理上传请求 的Controller对象
package com.jt.resource.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
@Slf4j
//@CrossOrigin //用于在controller层面处理跨域
@RefreshScope// //假如属性的值来自配置中心,配置中心内容变化,属性值也要变就需要添加上此注解。重新创建对象
@RestController
@RequestMapping("/resource")
public class ResourceController {
//当类的上面添加了@Slf4J就不用自己创建下面的日志对象了
// private static final Logger log=
// LoggerFactory.getLogger(ResourceController.class);
@Value("${jt.resource.path:g:/uploads}")
private String resourcePath;//="d:/uploads/";
@Value("${jt.resource.host:http://localhost:8881/}")
private String resourceHost;//="http://localhost:8881/";
/**
* 通过此方法处理文件上传的请求
* @param uploadFile 接收要上传的文件数据(参数名一定要与客户端提交的名字一样)
* @return 文件上传以后在服务端的实际存储路径,可以基于http协议访问这个文件
*/
@PostMapping("/upload/")//文件上传的请求方式必须是post,get上传会导致405,服务器与客户端请求方式不一样
public String uploadFile(MultipartFile uploadFile) throws IOException {
//1.创建文件的存储目录(按年月日的结构进行存储)
//1.1获取当前日期对应的字符串
//1.1.1方式1
//SimpleDateFormat sdf=new SimpleDateFormat("yyyy/MM/dd");
//String dateStr=sdf.format(new Date());
//1.1.2方式2(基于jdk8中提供的日期API)
String dateStr =
DateTimeFormatter.ofPattern("yyyy/MM/dd")
.format(LocalDate.now());
log.debug("date dir is {}",dateStr);
//1.2创建文件目录对象
File uploadDir=
new File(resourcePath,dateStr);//g:/uplods/2021/09/24
if(!uploadDir.exists())
uploadDir.mkdirs();
//2.给文件一个新的名字(文件前缀随机产生,文件后缀不能变)
//2.1获取原始文件名
String originalFilename =
uploadFile.getOriginalFilename();
//2.2构建文件前缀
String filePrefix=
UUID.randomUUID().toString();
//2.2获取文件后缀 xxx.png
String fileSuffix=
originalFilename.substring(
originalFilename.lastIndexOf("."));
//2.3构建新文件名
String newFileName=filePrefix+fileSuffix;
//3.上传文件到指定目录
//transferTo方法底层会做什么?(文件复制)
//基于inputStream读取uploadFile中的内容
//基于OutputStream将读取的内容写到新的文件中
uploadFile.transferTo(new File(uploadDir,newFileName));
//4.返回通过http协议可以访问到这个文件的路径
//String accessPath="http://localhost:8881/2021/09/24/xx.png";
String accessPath=resourceHost+dateStr+"/"+newFileName;
log.info("access path is {}", accessPath);
return accessPath;
}
}
跨域配置实现
package com.zlq.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsFilterConfig {
@Bean
public FilterRegistrationBean<CorsFilter> filterFilterRegistrationBean(){
CorsConfiguration config=new CorsConfiguration();
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource configSource=new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);
FilterRegistrationBean<CorsFilter> fBean =new FilterRegistrationBean<>(new CorsFilter(configSource));
fBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return null;
}
//方案二基于过滤器层面实现跨域配置
@Configuration
public class CorsFilterConfig2 {
@Bean
public FilterRegistrationBean<CorsFilter> filterFilterRegistrationBean(){
//1.对此过滤器进行配置(跨域设置-url,method)
//创建基于Url的核心配置服务
UrlBasedCorsConfigurationSource configService = new UrlBasedCorsConfigurationSource();
//设置核心配置
CorsConfiguration config = new CorsConfiguration();
//允许哪种请求头跨域
config.addAllowedHeader("*");
//允许哪种方法类型跨域 get post delete put
config.addAllowedMethod("*");
// 允许哪些请求源(ip:port)跨域
config.addAllowedOrigin("*");
//是否允许携带cookie跨域
config.setAllowCredentials(true);
//2.注册过滤器并设置其优先级
//配置服务.配置核心服务(配置路径,配置内容)
configService.registerCorsConfiguration("/****", config);
//过滤器Bean注册
FilterRegistrationBean fBean = new FilterRegistrationBean(
new CorsFilter(configService)
);
//设置其最高优先级
fBean.setOrder(Ordered.LOWEST_PRECEDENCE);
return fBean;
}
}
}
客户端工程逻辑实现
客户端工程基于springboot工程进行设计,项目上线时可以将其静态资源直接放到一个静态资源目录中.
添加依赖
web依赖
spring-boot-starter-web
构建项目启动类
构建启动类
创建文件上传页面
resource.static目录下,创建fileupload.html静态文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件上载演示</title>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<form id="fileForm" method="post" enctype="multipart/form-data" onsubmit="return doUpload()">
<div>
<label>上传文件
<input id="uploadFile" type="file" name="uploadFile">
</label>
</div>
<button type="submit">上传文件</button>
</form>
</body>
<script>
//jquery代码的表单提交事件
function doUpload(){
debugger //前端debug
//获得用户选中的所有图片(获得数组)
let files=document.getElementById("uploadFile").files;
if(files.length>0){
//获得用户选中的唯一图片(从数组中取出)
let file=files[0];
//开始上传这个图片
//由于上传代码比较多,不想和这里其它代码干扰,所以定义一个方法调用
upload(file);
}
//阻止表单提交效果
return false;
};
// 将file上传到服务器的方法
function upload(file){
//定义一个表单
let form=new FormData();
//将文件添加到表单中
form.append("uploadFile",file);
//异步提交
let url="http://localhost:8881/resource/upload/";
axios.post(url,form)
.then(function (response){
alert("upload ok")
console.log(response.data);
})
.catch(function (e){//失败时执行catch代码块
console.log(e);
})
}
</script>
</html>
启动服务访问测试
- nacos 服务注册和配置管理
- sca-resource 提供文件上传功能
- sca-resource-ui 客户端,提供静态资源的访问
- 浏览器访问
API网关(Gateway)工程实践
概述
API 网关是外部资源对服务内部资源访问的入口,所以文件上传请求应该首先请求的是网关服务,然后由网关服务转发到具体的资源服务上。
服务调用架构
工程项目结构设计
创建网关工程及初始化
-
创建工程
-
添加依赖
网关
spring-cloud-starter-gateway
nacos注册发现
spring-cloud-starter-alibaba-nacos-discovery
nacos配置
spring-cloud-starter-alibaba-nacos-config -
创建配置文件bootstrap.yml
server:
port: 9000
spring:
application:
name: sca-resource-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yml
gateway:
discovery:
locator:
enabled: true
routes:
- id: router01
uri: lb://sca-resource
predicates:
- Path=/sca/resource/upload/**
filters:
- StripPrefix=1
- 创建启动类,启动检测
网关跨域配置
基于Ajax技术(axios)访问网关时,需要网关层面跨域 注意注释掉服务器的网关
注意:网关使用的是netty和webflex,使用的包不同
package com.jt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
@Configuration
public class CorsFilterConfig {
@Bean
public CorsWebFilter corsWebFilter(){
//1.构建基于url方式的跨域配置
UrlBasedCorsConfigurationSource source= new UrlBasedCorsConfigurationSource();
//2.进行跨域配置
CorsConfiguration config=new CorsConfiguration();
//2.1允许所有ip:port进行跨域
config.addAllowedOrigin("*");
//2.2允许所有请求头跨域
config.addAllowedHeader("*");
//2.3允许所有请求方式跨域:get,post,..
config.addAllowedMethod("*");
//2.4允许携带有效cookie进行跨域
config.setAllowCredentials(true);
source.registerCorsConfiguration("/**",config);
return new CorsWebFilter(source);
}
}
Spring Gateway工程的跨域,除了网关的java代码,还可以配置文件跨域配置
spring:
cloud:
gateway:
globalcors: #跨域配置
corsConfigurations:
'[/**]':
allowedOrigins: "*"
allowedHeaders: "*"
allowedMethods: "*"
allowCredentials: true
启动工程进行服务访问
- 打开网关(Gateway)、资源服务器(Resource)、客户端工程服务(UI),修改html中的访问服务端路径
- 上传测试 注意:访问的是客户端网页
网关上对文件上传限流
第一步:在网关pom文件中添加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
第二步:在网关配置文件中添加sentinel配置
sentinel:
transport:
dashboard: localhost:8180
eager: true
第三步:在网关项目启动时,配置jvm启动参数,例如:
-Dcsp.sentinel.app.type=1
第四步:先执行一次上传,然后对上传进行限流规则设计
第五步:修改文件上传页面js,对限流结果进行处理,例如:
function upload(file){
//定义一个表单(axios中提供的表单对象)
let form=new FormData();
//将文件添加到表单中
form.append("uploadFile",file);
//异步提交(现在是提交到网关)
//let url="http://localhost:8881/resource/upload/"
let url="http://localhost:9000/sca/resource/upload/";
axios.post(url,form)
.then(function (response){
alert("upload ok")
console.log(response.data);
})
.catch(function (e){//失败时执行catch代码块
//被限流后服务端返回的状态码为429
if(e.response.status==429){
alert("上传太频繁了");
}
console.log("error",e);
})
}
第六步:启动服务进行文件上传测试,检测限流效果
AOP方式操作日志记录
页面描述
在实现文件上传业务时,添加记录日志的操作。
添加项目依赖
服务器添加AOP依赖
spring-boot-starter-aop
创建切入点注解
定义注解
package com.jt.resource.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredLog { //创建一个标记
String value() default "";
}
定义切入点方法
描述目标方法。该方法为连接点
@RequiredLog("文件上传")
@PostMapping("/upload/") //使用标记(注解) 描述一个方法
public String uploadFile(MultipartFile uploadFile) throws IOException {...}
定义日志操作切面
切面封装切入点(PointCut)和扩展业务逻辑(Around) 切面=切入点+Advice 编制:将切面与主要代码结合的技术
package com.jt.resource.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class LogAspect {
//定义切入点表达式
@Pointcut("@annotation(com.jt.resource.annotation.RequiredLog)")//注解方式的切入点表达式,细粒度
//@Pointcut("bean(resourceController)")粗粒度定义切入点表达式,这个类的所有方法都使用AOP
//@Pointcut("within(com.zlq.controller.ResourceController)")
//@Pointcut("within(com.zlq.controller.*)")
public void doLog(){}//锦上添花的锦(注解描述的方法)
//这里不写任何代码,只是为承载注解
//定义扩展业务逻辑
@Around("doLog()")
//@Around("@annotation(com.jt.resource.annotation.RequiredLog)")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.debug("Before {}",System.currentTimeMillis());
Object result=joinPoint.proceed();//执行执行链(其它切面,目标方法-锦)
log.debug("After {}",System.currentTimeMillis());
return result;//目标方法(切入点方法)的执行结果
}
}
AOP 方式日志记录原理分析
底层
定义AOP切面后。系统启动,对@Aspect注解描述的类进行加载分析,基于切入点的描述为目标类型对象,创建代理对象,并在代理对象内部创建一个执行链,这个执行链中包含拦截器(封装了切入点信息),通知(Around,…),目标对象等,我们请求目标对象资源时,会直接按执行链的顺序对资源进行调用。