业务描述

基于Spring Cloud Alibaba解决方案实现文件上传,例如

springcloudgateway增加转发服务后需要重启吗_html

工程创建及初始化

工程结构

参考如下工程结构,进行项目创建,例如:

springcloudgateway增加转发服务后需要重启吗_mvc_02

创建父工程

创建项目父工程用来管理项目依赖

springcloudgateway增加转发服务后需要重启吗_html_03

springcloudgateway增加转发服务后需要重启吗_html_04

创建文件服务工程 resource

创建用于处理文件上传业务的工程,例如:

springcloudgateway增加转发服务后需要重启吗_spring_05

springcloudgateway增加转发服务后需要重启吗_mvc_06

创建客户端服务工程 resource-ui

创建一个客户端工程,在此工程中定义一些静态页面,例如文件上传页面:

springcloudgateway增加转发服务后需要重启吗_spring_07

springcloudgateway增加转发服务后需要重启吗_跨域_08

父工程初始化

打开父工程的pom.xml文件,添加如下依赖:

<!--定义java编译版本-->
 <properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
 </properties>

 <dependencyManagement>
        <dependencies>
        
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.3.2.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR9</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.2.6.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            
        </dependencies>
    </dependencyManagement>

文件资源服务实现

添加项目依赖

在sca-resource工程中添加如下依赖:

<!--Spring Boot Web (服务-内置tomcat)-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!--Nacos Discovery (服务注册发现)-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        
        <!--Nacos Config (配置中心)-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        
        <!--Sentinel (流量防卫兵-限流和熔断)-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        
        <!--Spring Boot 监控-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
            <scope>provided</scope>
        </dependency>

服务初始化配置

server:
  port: 8500
spring:
  application:
    name: sca-resource
  resources:    #定义可以访问到上传资源的路径
    static-locations: file:d:/uploads    
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848

#自定义上传的资源地址和服务器地址
jt:
  resource:
    path: d:/uploads/ #设计上传文件存储的根目录(后续要写到配置文件)
    host: http://localhost:8881/ #定义上传文件对应的访问服务器

构建项目启动类

package com.jt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class FileApplication {
    public static void main(String[] args) {
        SpringApplication.run(FileApplication.class, args);
    }
}

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
@RestController
@RequestMapping("/resource/")
public class ResourceController {
      //当了类的上面添加了@Slf4J就不用自己创建下面的日志对象了
//    private static final Logger log=
//            LoggerFactory.getLogger(ResourceController.class);

     @Value("${jt.resource.path}")
     private String resourcePath;//="d:/uploads/";
     @Value("${jt.resource.host}")
     private String resourceHost;//="http://localhost:8881/";

     @PostMapping("/upload/")
     public String uploadFile(MultipartFile uploadFile) throws IOException {
         //1.创建文件存储目录(按时间创建-yyyy/MM/dd)
         //1.1获取当前时间的一个目录
         String dateDir = DateTimeFormatter.ofPattern("yyyy/MM/dd")
                 .format(LocalDate.now());
         //1.2构建目录文件对象
         File uploadFileDir=new File(resourcePath,dateDir);
         if(!uploadFileDir.exists())uploadFileDir.mkdirs();
         //2.给文件起个名字(尽量不重复)
         //2.1获取原文件后缀
         String originalFilename=uploadFile.getOriginalFilename();
         String ext = originalFilename.substring(
                 originalFilename.lastIndexOf("."));
         //2.2构建新的文件名
         String newFilePrefix=UUID.randomUUID().toString();
         String newFileName=newFilePrefix+ext;
         //3.开始实现文件上传
         //3.1构建新的文件对象,指向实际上传的文件最终地址
         File file=new File(uploadFileDir,newFileName);
         //3.2上传文件(向指定服务位置写文件数据)
         uploadFile.transferTo(file);
         String fileRealPath=resourceHost+dateDir+"/"+newFileName;
         log.debug("fileRealPath {}",fileRealPath);
         //后续可以将上传的文件信息写入到数据库?
         return fileRealPath;
     }
}

跨域配置实现

我们在通过客户端工程,访问文件上传服务时,需要进行跨域配置,例如:

package com.jt.files.config;

/**
 * 跨域配置(基于过滤器方式进行配置,并且将过滤优先级设置高一些)
 */
@Configuration
public class CorsFilterConfig {
    @Bean
    public FilterRegistrationBean<CorsFilter> filterFilterRegistrationBean(){
        //1.对此过滤器进行配置(跨域设置-url,method)
        UrlBasedCorsConfigurationSource configSource=new UrlBasedCorsConfigurationSource();
        CorsConfiguration config=new CorsConfiguration();
         //允许哪种请求头跨域
        config.addAllowedHeader("*");
        //允许哪种方法类型跨域 get post delete put
        config.addAllowedMethod("*");
        // 允许哪些请求源(ip:port)跨域
        config.addAllowedOrigin("*");
        //是否允许携带cookie跨域
        config.setAllowCredentials(true);
        //2.注册过滤器并设置其优先级
        configSource.registerCorsConfiguration("/**", config);
        FilterRegistrationBean<CorsFilter> fBean= new FilterRegistrationBean(new CorsFilter(configSource));
        fBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return fBean;
    }
}

客户端工程逻辑实现

添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

构建项目启动类

package com.jt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(ClientApplication .class, args);
    }
}

创建文件上传页面

定义文件上传页面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(){
        //获得用户选中的所有图片(获得数组)
        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服务

第四步:打开浏览器进行访问测试,例如:

springcloudgateway增加转发服务后需要重启吗_html_09

springcloudgateway增加转发服务后需要重启吗_跨域_10

springcloudgateway增加转发服务后需要重启吗_上传_11

API网关(Gateway)工程实践

概述

API 网关是外部资源对服务内部资源访问的入口,所以文件上传请求应该首先请求的是网关服务,然后由网关服务转发到具体的资源服务上。

服务调用架构

springcloudgateway增加转发服务后需要重启吗_mvc_12

工程项目结构设计

springcloudgateway增加转发服务后需要重启吗_mvc_13

创建网关工程及初始化

第一步:创建sca-resource-gateway工程,例如:

springcloudgateway增加转发服务后需要重启吗_html_14

第二步:添加项目依赖,例如:

<dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>

第三步:创建配置文件bootstrap.yml,然后进行初始配置,例如:

package com.jt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ResourceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ResourceApplication.class,args);
    }
}

网关跨域配置

当我们基于Ajax技术访问网关时,需要在网关层面进行跨域设计,例如:

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),然后修改fileupload.html文件中访问资源服务端的url,例如:

let url="http://localhost:9999/nacos/resource/upload/";

接下来进行访问测试,例如:

springcloudgateway增加转发服务后需要重启吗_上传_15

springcloudgateway增加转发服务后需要重启吗_html_16

springcloudgateway增加转发服务后需要重启吗_html_17

网关限流

第一步:添加sentinel与gateway的依赖:

<!--  添加 sentinel与gateway依赖 进行限流-->
  <!--网关层面的sentinel限流-->
     <dependency>
         <groupId>com.alibaba.cloud</groupId>
         <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
     </dependency>
     
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-actuator</artifactId>
     </dependency>
     
     <dependency>
         <groupId>com.alibaba.cloud</groupId>
         <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
     </dependency>

第二步:配置yml文件

sentinel:
      transport:
        dashboard: localhost:8180
      eager: true #服务齐动手会向sentinel控制台发送心跳,为我们的应用创建菜单

第三步:启动网关项目,检测sentinel控制台的网关菜单。

启动时,添加sentinel的jvm参数,通过此菜单可以让网关服务在sentinel控制台显示不一样的菜单,代码如下。

-Dcsp.sentinel.app.type=1

springcloudgateway增加转发服务后需要重启吗_spring_18

springcloudgateway增加转发服务后需要重启吗_mvc_19

做个简单的限流,五秒限流一次

springcloudgateway增加转发服务后需要重启吗_html_20

springcloudgateway增加转发服务后需要重启吗_mvc_21

做个简单的异常处理

springcloudgateway增加转发服务后需要重启吗_上传_22

springcloudgateway增加转发服务后需要重启吗_spring_23

AOP方式操作日志记录

添加项目依赖

在sca-resource工程中添加AOP依赖,例如:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

yml文件中配置日志级别(可默认)

logging:
  level:
    com.deven: debug

1.日志级别:

trace<debug<info<warn<error<fatal

显示大于等于设置的日志级别,例如,设置info ,则显示info , warn, error ,fatal ,不显示 trace和debug

2.作用范围

logging:
	level:
		root=info

这里的root代表根目录,作用范围是整个项目,也可以将root修改为具体的包名,则作用范围为该包下面的所有类。

例如:

logging:
	level:
		com.example.controller=debug

则com.example.controller包下面的所有类的debug ,info ,warn ,error ,fatal 日志可以显示

也可以多个设置叠加使用,

例如:

logging:
	level:
		root=warn

logging:
	level:
		com.example.controller=debug

则com.example.controller包下面的所有类的debug ,info ,warn ,error ,fatal 日志可以显示

其他类只显示warn ,error ,fatal 日志

创建切入点注解

我们项目要为目标业务实现功能增强,锦上添花,但系统要指定谁是目标业务,这里我们需要自定义一个注解,后续用此注解描述目标业务。

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,对sca-resources工程中的ResourceController文件上传方法进行描述,例如:

@RequiredLog("upload file")
@PostMapping("/upload/")
public String uploadFile(MultipartFile uploadFile) throws IOException {...}

springcloudgateway增加转发服务后需要重启吗_html_24

定义日志操作切面

在AOP编程设计中,我们会通过切面封装切入点(Pointcut)和扩展业务逻辑(Around,…)的定义,例如:

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 {
    
    //定义切入点
     /**
     * 1.
     * @Pointcut中定义切入点表达式
     * 表达式中要描述在哪些地方定义切入点
     * 表达式的定义有多中写法,在这里我采用注解方式的表达式
     * */
    @Pointcut("@annotation(com.jt.resource.annotation.RequiredLog)")
    public void doLog(){}//锦上添花的锦(注解描述的方法)

    //定义扩展业务逻辑
       /**
     * 2.
     * @Around 通知方法
     * 在执行的切入点方法上执行@Around注解描述的方法
     * ProceedingJoinPoint 程序连接点(封装了你要执行的执行链信息,包括目标方法信息)
     * @return 目标方法的返回值
     * */
    @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;//目标方法(切入点方法)的执行结果
    }
}

测试日志打印

springcloudgateway增加转发服务后需要重启吗_上传_25


日志输出

springcloudgateway增加转发服务后需要重启吗_mvc_26

AOP 方式日志记录原理分析

我们在基于AOP方式记录用户操作日志时,其底层工作流程如下:

springcloudgateway增加转发服务后需要重启吗_mvc_27

说明:当我们在项目中定义了AOP切面以后,系统启动时,会对有@Aspect注解描述的类进行加载分析,基于切入点的描述为目标类型对象,创建代理对象,并在代理对象内部创建一个执行链,这个执行链中包含拦截器(封装了切入点信息),通知(Around,…),目标对象等,我们请求目标对象资源时,会直接按执行链的顺序对资源进行调用。

  • 拦截器调用切面对象
  • 切面调用目标方法