【SpringBoot】2022-03-26【自定义请求转发、分发】

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了【SpringBoot】2022-03-26【自定义请求转发、分发】相关的知识,希望对你有一定的参考价值。

参考技术A

当后端需要部署多区域或者多实例,而前端界面是一个,往往通过前端的区域筛选器来切换访问对应区域的后端实例时。可以通过前端直接访问不同区域的Ip,但这样新增区域或者后端变化时不够灵活;另外,也可以通过访问形如注册中心的转发服务,转发服务根据请求中的区域字段获取对应后端的地址进而转发到对应区域的后端,拿到接口返回数据后返回给前端。这样前端只需要配置一个访问地址,即转发服务的地址。

各区域的后端实例可以在启动时,将本机服务信息注册到zk中,这样转发服务就可以从zk中获取提供服务的后端地址。

对request对象进行包装:CustomHttpServletRequestWrapper.class

过滤器,拦截所有前端请求并进行转发,DispatcherFilter.class

具体的转发实现,DispatcherService.class

项目中定义的返回实体,OResult.class

附后端启动时将服务信息注册到zk的实现,ServerRegistry.class

本文有很多处可以以更好的方式来实现,比如:zk可以用watch等。
待补充

SpringBoot入门到精通-SpringBoot启动流程

定义自己的starter


1.认识SpringApplication

SpringApplication 类提供了一种可通过运行 main() 方法来启动 Spring 应用的简单方式。多数情况下,您只需要委托给静态的 SpringApplication.run 方法:

public static void main(String[] args) 
    SpringApplication.run(MySpringConfiguration.class, args);

如果 SpringApplication 的默认设置不符合您的想法,您可以创建本地实例进行定制化。例如,要关闭 banner,您可以这样:

public static void main(String[] args) 
    SpringApplication app = new SpringApplication(MySpringConfiguration.class);
    app.setBannerMode(Banner.Mode.OFF);
    app.run(args);

2.SpringApplication.run执行流程

SpringApplication可用于从 Java 主方法引导和启动 Spring 应用程序的类。默认情况下,类将执行以下步骤来启动应用:

  1. 创建一个适当的[ApplicationContext]实例(取决于您的类路径)

  2. 注册 [CommandLinePropertySource]以将命令行参数公开为 Spring 属性

  3. 刷新应用程序上下文,加载所有单例 bean

  4. 触发任何[CommandLineRunner]bean

下面我们就来详细分析一下它的执行流程,见:org.springframework.boot.SpringApplication#run(java.lang.String…)

public ConfigurableApplicationContext run(String... args) 
		//创建秒表,用来计算启动事件
		StopWatch stopWatch = new StopWatch();
    	//启动秒表
		stopWatch.start();
         //Spring IOC 容器对象
		ConfigurableApplicationContext context = null;
         //收集Spring Boot 异常报告器的list
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    	//配置无头属性,java.awt.headless
		configureHeadlessProperty();
    	//SpringBoot的SpringApplication run方法的侦听器 监听器,
    	//SpringApplicationRunListeners维护了一个 SpringApplicationRunListener 集合
		SpringApplicationRunListeners listeners = getRunListeners(args);
    	//会触发所有 SpringApplicationRunListener#starting的执行
         //,会通过SimpleApplicationEventMulticaster广播一个ApplicationStartingEvent事件
		listeners.starting();
		try 
             //把应用参数封装到DefaultApplicationArguments,通过它可以访问应用参数
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
             //创建环境对象,Environment包括了property和profile
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
            //配置忽略 Bean 信息 ,spring.beaninfo.ignore
			configureIgnoreBeanInfo(environment);
            //打印横幅
			Banner printedBanner = printBanner(environment);
            //创建IOC容器对象 AnnotationConfigApplicationContext
			context = createApplicationContext();
             //创建Spring Boot 异常报告器实例。会扫描spring.factories下的 FailureAnalyzers实例,
            //FailureAnalyzer是用于分析故障并提供可显示给用户的诊断信息
            //比如:NoSuchBeanDefinitionFailureAnalyzer ; DataSourceBeanCreationFailureAnalyzer
			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class[]  ConfigurableApplicationContext.class , context);
            //刷新容器准备工作
            //1.把environment绑定到context容器对象
            //2.context后置处理,比如绑定resourceLoader
            //3.触发 ApplicationContextInitializer#initialize初始化(用于在刷新之前初始化Context回调接口。)
            //4.触发 listener.contextPrepared ,抛出 ApplicationContextInitializedEvent 事件
            //5.把ApplicationArguments注册到容器中成为一个Bean
            //6.把 Banner注册到容器中成为一个Bean
			prepareContext(context, environment, listeners, applicationArguments, printedBanner);
            //刷新容器,底层走spring的刷新容器流程
			refreshContext(context);
            //空方法,留给我们扩展
			afterRefresh(context, applicationArguments);
            //暂定秒表
			stopWatch.stop();
			if (this.logStartupInfo) 
                //打印秒表记录的时间
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			
            //触发 SpringApplicationRunListener#started方法抛出 ApplicationStartedEvent 事件
			listeners.started(context);
            //调用 ApplicationRunner 和 CommandLineRunner
			callRunners(context, applicationArguments);
		
		catch (Throwable ex) 
            //处理异常,会从exceptionReporters拿出异常进行打印
            //以及会触发 SpringApplicationRunListeners#failed,广播 ApplicationFailedEvent事件
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		

		try 
                //执行listeners.running , 抛出 ApplicationReadyEvent 事件
			listeners.running(context);
		
		catch (Throwable ex) 
               //处理异常,会从exceptionReporters拿出异常进行打印
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		
    	//返回容器
		return context;
	

画一个流程图总结一下

3.StopWatch秒表

Spring体用的秒表,允许对多个任务进行计时,显示每个命名任务的总运行时间和运行时间。隐藏System.nanoTime()的使用,提高应用程序代码的可读性并减少计算错误的可能性。注意,此对象并非设计为线程安全的,也不使用同步。

public class StopWatch 
		/**
	 * Identifier of this @code StopWatch.
	 * <p>Handy when we have output from multiple stop watches and need to
	 * distinguish between them in log or console output.
	 */
    //任务的ID
	private final String id;

	private boolean keepTaskList = true;
    //任务列表
	private final List<TaskInfo> taskList = new LinkedList<>();

	/** Start time of the current task. */
    //开始时间
	private long startTimeNanos;

	/** Name of the current task. */
    //当前任务名
	@Nullable
	private String currentTaskName;

	@Nullable
	private TaskInfo lastTaskInfo;
    //任务数量
	private int taskCount;

	/** Total running time. */
    //总时间
	private long totalTimeNanos;
    
    //开始任务,穿了一个“”作为taskName
    public void start() throws IllegalStateException 
		start("");
	

	/**
	 * Start a named task.
	 * <p>The results are undefined if @link #stop() or timing methods are
	 * called without invoking this method first.
	 * @param taskName the name of the task to start
	 * @see #start()
	 * @see #stop()
	 */
    //开始任务
	public void start(String taskName) throws IllegalStateException 
		if (this.currentTaskName != null) 
			throw new IllegalStateException("Can't start StopWatch: it's already running");
		
        //任务名
		this.currentTaskName = taskName;
        //记录开始时间
		this.startTimeNanos = System.nanoTime();
	
    
    //停止秒表
    public void stop() throws IllegalStateException 
		if (this.currentTaskName == null) 
			throw new IllegalStateException("Can't stop StopWatch: it's not running");
		
        //时间差
		long lastTime = System.nanoTime() - this.startTimeNanos;
        //累计时间
		this.totalTimeNanos += lastTime;
        //创建一个TaskInfo任务信息
		this.lastTaskInfo = new TaskInfo(this.currentTaskName, lastTime);
		if (this.keepTaskList) 
            //加入任务列表
			this.taskList.add(this.lastTaskInfo);
		
        //增加任务数量
		++this.taskCount;
        //清空任务名
		this.currentTaskName = null;
	
    //以优雅的格式打印秒表记录的时间日志
    public String prettyPrint() 
		StringBuilder sb = new StringBuilder(shortSummary());
		sb.append('\\n');
		if (!this.keepTaskList) 
			sb.append("No task info kept");
		
		else 
			sb.append("---------------------------------------------\\n");
			sb.append("ns         %     Task name\\n");
			sb.append("---------------------------------------------\\n");
			NumberFormat nf = NumberFormat.getNumberInstance();
			nf.setMinimumIntegerDigits(9);
			nf.setGroupingUsed(false);
			NumberFormat pf = NumberFormat.getPercentInstance();
			pf.setMinimumIntegerDigits(3);
			pf.setGroupingUsed(false);
			for (TaskInfo task : getTaskInfo()) 
				sb.append(nf.format(task.getTimeNanos())).append("  ");
				sb.append(pf.format((double) task.getTimeNanos() / getTotalTimeNanos())).append("  ");
				sb.append(task.getTaskName()).append("\\n");
			
		
		return sb.toString();
	

StopWatch秒表可以用来对多个任务计时,,start的时候会使用System.nanoTime()来获时间记录到startTimeNanos ,stop结束方计算时间差,然后会把每次的时间和任务名封装成TaskInfo,加入taskList。最后会累计每次任务的时间总额。提供了prettyPrint方法以优雅的格式组织秒表记录的时间日志。

但是要注意:虽然它可以允许多个任务记时,但是它并不是线程安全的。

4.SpringBootExceptionReporter异常报告

4.1.核心类认识

SpringBootExceptionReporter是用于支持自定义上报SpringApplication启动错误的回调接口,它可以把启动的错误日志汇报给用户

@FunctionalInterface
public interface SpringBootExceptionReporter 

	/**
	 * Report a startup failure to the user.
	 * @param failure the source failure
	 * @return @code true if the failure was reported or @code false if default
	 * reporting should occur.
	 */
	boolean reportException(Throwable failure);


reportException方法的作用就是为用户报告错误。它的唯一实现类是 FailureAnalyzers ,它提供了

	
final class FailureAnalyzers implements SpringBootExceptionReporter 

	private static final Log logger = LogFactory.getLog(FailureAnalyzers.class);

	private final ClassLoader classLoader;
    //故障分析仪
	private final List<FailureAnalyzer> analyzers;
    
    //报告指定的异常
    @Override
	public boolean reportException(Throwable failure) 
        //把异常封装到FailureAnalysis
        //FailureAnalysis中维护了很多的FailureAnalyzer,它的作用是分析故障并提供可显示给用户的诊断信息
		FailureAnalysis analysis = analyze(failure, this.analyzers);
		return report(analysis, this.classLoader);
	
    
    //分析异常
	private FailureAnalysis analyze(Throwable failure, List<FailureAnalyzer> analyzers) 
		for (FailureAnalyzer analyzer : analyzers) 
			try 
                //把Throwable异常信息封装成FailureAnalysis
				FailureAnalysis analysis = analyzer.analyze(failure);
				if (analysis != null) 
					return analysis;
				
			
			catch (Throwable ex) 
				logger.debug(LogMessage.format("FailureAnalyzer %s failed", analyzer), ex);
			
		
		return null;
	
    
	private boolean report(FailureAnalysis analysis, ClassLoader classLoader) 
        //加载FailureAnalysisReporter,  FailureAnalysisReporter用来 向用户报告FailureAnalysis分析。
		List<FailureAnalysisReporter> reporters = SpringFactoriesLoader.loadFactories(FailureAnalysisReporter.class,
				classLoader);
		if (analysis == null || reporters.isEmpty()) 
			return false;
		
		for (FailureAnalysisReporter reporter : reporters) 
            //报告异常
			reporter.report(analysis);
		
		return true;
	

reportException方法接收一个Throwable ,然后Throwable 会被封装到FailureAnalysis。然后通过SpringFactoriesLoader去加载FailureAnalysisReporter(向用户报告FailureAnalysis分析),通过FailureAnalysisReporter去报告异常。FailureAnalysis结构如下

public class FailureAnalysis 
	//异常描述
	private final String description;
	
	private final String action;
	//异常对象
	private final Throwable cause;

LoggingFailureAnalysisReporter结构如下见:org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter#report

public final class LoggingFailureAnalysisReporter implements FailureAnalysisReporter 

	private static final Log logger = LogFactory.getLog(LoggingFailureAnalysisReporter.class);

@Override
	public void report(FailureAnalysis failureAnalysis) 
        
		if (logger.isDebugEnabled()) 
			logger.debug("Application failed to start due to an exception", failureAnalysis.getCause());
		
        //把错误日志打印到控制台
		if (logger.isErrorEnabled()) 
			logger.error(buildMessage(failureAnalysis));
		
	
	//构建错误日志内容
	private String buildMessage(FailureAnalysis failureAnalysis) 
		StringBuilder builder = new StringBuilder();
		builder.append(String.format("%n%n"));
		builder.append(String.format("***************************%n"));
		builder.append(String.format("APPLICATION FAILED TO START%n"));
		builder.append(String.format("***************************%n%n"));
		builder.append(String.format("Description:%n%n"));
		builder.append(String.format("%s%n", failureAnalysis.getDescription()));
		if (StringUtils.hasText(failureAnalysis.getAction())) 
			builder.append(String.format("%nAction:%n%n"));
			builder.append(String.format("%s%n", failureAnalysis.getAction()));
		
		return builder.toString();
	

4.2.报告异常

在SpringApplication#run方法中有try-catch操作,如果启动出现异常,会执行org.springframework.boot.SpringApplication#handleRunFailure来处理异常

private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception,
			Collection<SpringBootExceptionReporter> exceptionReporters, SpringApplicationRunListeners listeners) 
		try 
			try 
                //处理退出码,发布一个ExitCodeEvent事件
				handleExitCode(context, exception);
				if (listeners != null) 
                    //发布ApplicationFailedEvent事件
					listeners.failed(context, exception);
				
			
			finally 
                //报告异常,通过LoggingFailureAnalysisReporter 把异常打印到控制台
				reportFailure(exceptionReporters, exception);
				if (context != null) 
					context.close();
				
			
		
		catch (Exception ex) 
			logger.warn("Unable to close ApplicationContext", ex);
		
		ReflectionUtils.rethrowRuntimeException(exception);
	

上面重要是发布ApplicationFailedEvent事件, 然后通过SpringBootExceptionReporter#reportException去把异常打印到控制台,

5.监听器机制

上面代码中有很多地方都出现了事件发布,比如: SpringApplicationRunListeners listeners = getRunListeners(args) 它的作用是广播ApplicationStartingEvent事件,这用到了Spring的监听器机制。我们可以认为以 Listenner 结尾的类都是监听器,监听器使用到了观察者设计模式,其作用是监听一些事件的发生从而进行一些操作。监听器的好处是可以实现代码解耦,对此你可能不是很能理解,我这里用一个js例子来代理理解事件机制

function dothing()
    //回调函数

//监听button的click事件
$("#button").click(dothing);

上面代码相信你是写过的,就是一个JS监听按钮点击事件,这里需要明确三个角色

  • button : 事件源,这个事件发生在谁身上
  • click : 事件类型 ,按钮发生了什么事件
  • dothing : 回调函数,当button被点击,触发 dothing函数。

那么Java中的事件机制和上面案例很相似,我这里有个案例:当用户注册成功,给用户推送一条短信,使用事件机制来实现

这么理解这幅图

  1. 首先需要定义一个事件类型RegisterApplicationEvent 继承于ApplicationEvent , 代表的注册这个事件,好比是"click"
  2. 然后需要在注册逻辑中,使用事件发布器ApplicationEventPublisher 发布该事件 ,好比 button 被 click了
  3. 事件被发布,需要触发某段逻辑,所以要写一个监听器类实现ApplicationListernner,该监听器监听的是“注册事件”。
  4. 然后就调用短信发送逻辑发送短信即可。好比是上面的dothing回调函数。

相信大致的流程你是看懂了,但是有些陌生类让我们比较迷惑,下面我们就来系统的认识一下这些类。

5.1. 核心类认识

EventListener

EventListener是java提供的最顶层的监听器接口,不管是Servlet的监听器还是Spring的监听器都是该接口的子类(所有事件侦听器接口都必须实现于接口)。

/**
 * A tagging interface that all event listener interfaces must extend.
 * @since JDK1.1
 */
public interface EventListener