zoukankan      html  css  js  c++  java
  • SpringBoot深入理解

    SpringBoot深入理解

    项目打包SpringBoot启动过程

    当使用打包时,会下载org-springframework-boot-loader的jar,并且不会放在lib存放的第三方jar包文件中,该jar包中有个JarLauncher.class文件中设置了jar包运行时的入口和打包后文件的结构(定义了BOOT-INF,META-INF中放什么数据)

    使用java -jar 启动项目时,因为META-INF中文件记载了启动的顺序

    Manifest-Version: 1.0		#版本
    Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx		
    Start-Class: com.zyy.gradletest1.GradleTest1Application	     #项目的启动器
    Spring-Boot-Classes: BOOT-INF/classes/		#记录编译后文件存放地址
    Spring-Boot-Lib: BOOT-INF/lib/						  #记录第三方jar包存放地址
    Spring-Boot-Version: 2.3.0.RELEASE		        #SpringBoot版本
    Main-Class: org.springframework.boot.loader.JarLauncher			#标记了程序的入口,程序的入口定义类加载器去加载项目的启动器
    
    

    所以程序会直接进入JarLauncher.class中执行main方法

    public class JarLauncher extends ExecutableArchiveLauncher {
    
    	static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
    
    	static final String BOOT_INF_LIB = "BOOT-INF/lib/";
    
    	public JarLauncher() {
    	}
    
    	protected JarLauncher(Archive archive) {
    		super(archive);
    	}
    
    	@Override
    	protected boolean isNestedArchive(Archive.Entry entry) {
    		if (entry.isDirectory()) {
    			return entry.getName().equals(BOOT_INF_CLASSES);
    		}
    		return entry.getName().startsWith(BOOT_INF_LIB);
    	}
    
    	public static void main(String[] args) throws Exception {
    		new JarLauncher().launch(args);
    	}
    
    }
    
    

    JarLancher继承了ExecutableArchiveLauncher,ExecutableArchiveLauncher抽象类是归档启动器的父类,它有两个子类

    ​ Lancher:是整个归档的基类(抽象类)

    ​ |

    ExecutableArchiveLauncher

    ​ | |

    JarLancher WarLancher

    顾名思义jarLancher就是打包成jar的归档启动类,WarLancher是War打包方式的归档启动类。

    当执行JarLancher的main方法时,会调用Lancher基类的launch方法,该方法注释为:启动应用程序。 此方法是初始入口点应该由一个子类被称为public static void main(String[] args)方法。

    image-20200530094047755


    使用SpringBoot自定义类加载器(LaunchedURLClassLoader)来加载打包好的Jar文件中BOOT-INF里面的类和lib

    ClassLoader classLoader = createClassLoader(getClassPathArchives());
    

    getClassPathArchives()方法:返回嵌套Archive S表示匹配指定过滤器条目。

    意思就是:获得需要自定义类加载器需要加载的类的路径并返回(归档文件)

    public abstract class ExecutableArchiveLauncher extends Launcher {
        @Override
        protected List<Archive> getClassPathArchives() throws Exception {
           List<Archive> archives = new ArrayList<>(
                 /*this::isNestedArchive:确定指定JarEntry是应添加到类路径嵌套项。 该方法被调用一次为每个条目。
                     说人话就是查看当前需要加载的路径是否符合,如果符合返回true,这个方法调用的就是JarLancher类				  中的isNestedArchive方法*/
                 this.archive.getNestedArchives(this::isNestedArchive));
           postProcessClassPathArchives(archives);
            //返回Archive类型的集合,Archive是可以由{@link Launcher}启动的档案类,里面记录的需要加载类的路径
           return archives;
        }
    }
    

    补充知识:

    正常的jar包加载的方式就是直接找到classpath下的顶层路径加载(例如:org.springframework)

    传统的打包方式是将第三方包和自己写的代码打包在一块,这就有可能出现,包名冲突之类的。为了解决这种问题,Spring的打包方式就是把第三方包和自己的代码分开。但是此时就出现了一个问题,不能正常找到顶层的加载文件,那么spring就出现了org-springframework-boot-loader包,spring默认规定该包在打包时将该包放到classpath下(顶层路径),首先加载该类中对应的Lancher类,然后通过该类创建的自定义类加载器去加载项目中其他的类。


    准备去加载项目的主运行程序

    launch(args, getMainClass(), classLoader);
    

    getMainClass():获得ExecutableArchiveLauncher.Archive类中的清单,Archive在创建自定义类加载器时,被构造方法初始化,初始化的路径就是classpath路径下的META-INF文件中的信息然后读取对应的主程序入口的路径(也就是META-INF文件中的信息)key为Start-Class的值(就是项目主程序的启动器)

    public abstract class ExecutableArchiveLauncher extends Launcher {
    
    	private final Archive archive;
    
    	public ExecutableArchiveLauncher() {
    		try {
    			this.archive = createArchive();
    		}
    		catch (Exception ex) {
    			throw new IllegalStateException(ex);
    		}
    	}
        @Override
        protected String getMainClass() throws Exception {
           Manifest manifest = this.archive.getManifest();
           String mainClass = null;
           if (manifest != null) {
              mainClass = manifest.getMainAttributes().getValue("Start-Class");
           }
           if (mainClass == null) {
              throw new IllegalStateException(
                    "No 'Start-Class' manifest entry specified in " + this);
           }
           return mainClass;
        }
    }
    

    当所有参数准备完毕之后,进入launch方法中

    public abstract class Launcher {
        /**
         * Launch the application given the archive file and a fully configured classloader.
         * @param args the incoming arguments
         * @param mainClass the main class to run
         * @param classLoader the classloader
         * @throws Exception if the launch fails
         */
        protected void launch(String[] args, String mainClass, ClassLoader classLoader)
              throws Exception {
            //将当前线程的上下文设置为自定义类加载器
           Thread.currentThread().setContextClassLoader(classLoader);
           createMainMethodRunner(mainClass, args, classLoader).run();
        }
    }
    

    创建一个运行Main的县城,将运行产参数和类加载器传入进去
    createMainMethodRunner(mainClass, args, classLoader)

    /**
     * Create the {@code MainMethodRunner} used to launch the application.
     * @param mainClass the main class					main方法运行的路径
     * @param args the incoming arguments		  方法的参数
     * @param classLoader the classloader			 自定义类加载器
     * @return the main method runner				返回线程
     */
    protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args,
          ClassLoader classLoader) {
       return new MainMethodRunner(mainClass, args);
    }
    

    当线程创建好之后!!!重点来了!!!!运行run方法

    createMainMethodRunner(mainClass, args, classLoader).run();

    public void run() throws Exception {
        //获得线程上下文取出类加载器,然后获得要运行的main方法的路径
       Class<?> mainClass = Thread.currentThread().getContextClassLoader()
             .loadClass(this.mainClassName);
        //通过反射定位到main方法的位置
       Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
        //调用对应的main方法(此时调用的就是SpringBoot项目的启动器)
       mainMethod.invoke(null, new Object[] { this.args });
    }
    

    注意:mainMethod.invoke(null, new Object[] { this.args });

    ​ 为什么invoke第一个参数是空,因为main方法是static的,static不归存与类中,它只是把类当做寄存的场所而已,原因是所有的方法必须存在与类中

    jar包启动时,使用-agentlib:jdwp远程debug

    什么是jdwep?

    JDWP 是 Java Debug Wire Protocol 的缩写,它定义了调试器(debugger)和被调试的 Java 虚拟机(target vm)之间的通信协议,可以项目在线运行时,远程debug查看运行的流程,如果项目是部署在tomcat中运行,需要在tomcat中配置相关的启动,才能使用远程debug

    启动jar包时,使用命令

    然后在idea中配置

    注意:这种调试需要在配置之前在idea中有相同的代码,然后打上断点

    注解

    java.lang.annotation 提供了四种元注解,专门注解其他的注解(在自定义注解的时候,需要使用到元注解):
    @Documented – 注解是否将包含在JavaDoc中
    @Retention – 什么时候使用该注解
    @Target – 注解用于什么地方
    @Inherited – 是否允许子类继承该注解

    1.)@Retention – 定义该注解的生命周期
    RetentionPolicy.SOURCE : 在编译阶段丢弃。这些注解在编译结束之后就不再有任何意义,所以它们不会写入字节码。@Override, @SuppressWarnings都属于这类注解。
    RetentionPolicy.CLASS : 在类加载的时候丢弃。在字节码文件的处理中有用。注解默认使用这种方式
    RetentionPolicy.RUNTIME : 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息。我们自定义的注解通常使用这种方式。

    2.)Target – 表示该注解用于什么地方。默认值为任何元素,表示该注解用于什么地方。可用的ElementType 参数包括
    ElementType.CONSTRUCTOR: 用于描述构造器
    ElementType.FIELD: 成员变量、对象、属性(包括enum实例)
    ElementType.LOCAL_VARIABLE: 用于描述局部变量
    ElementType.METHOD: 用于描述方法
    ElementType.PACKAGE: 用于描述包
    ElementType.PARAMETER: 用于描述参数
    ElementType.TYPE: 用于描述类、接口(包括注解类型) 或enum声明

    3.)@Documented – 一个简单的Annotations 标记注解,表示是否将注解信息添加在java 文档中。

    4.)@Inherited – 定义该注释和子类的关系
    @Inherited 元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited 修饰的annotation 类型被用于一个class,则这个annotation 将被用于该class 的子类。

    @Configuration官方文档

    修饰一个类为配置类,将Bean注入到Spring容器中,在运行期间生成bean的定义

    @Configuration
       public class AppConfig {
      
           @Bean
           public MyBean myBean() {
               // instantiate, configure and return bean ...
           }
       }
    

    @Configuration类通常使用AnnotationConfigApplicationContext或者支持web的AnnotationConfigWebApplicationContext的引导,来使用@Bean

    //在test类中可以使用,或者直接使用@Autowired自动注入
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
       ctx.register(AppConfig.class);
       ctx.refresh();
       MyBean myBean = ctx.getBean(MyBean.class);
       // use myBean ...
    

    @PropertySource

    @Configuration类可以使用@PropertySource注释将属性源贡献给环境对象:

    @Configuration
       @PropertySource("classpath:/com/acme/app.properties")
       public class AppConfig {
      
           @Inject Environment env;
      
           @Bean
           public MyBean myBean() {
               return new MyBean(env.getProperty("bean.name"));
           }
       }
    

    @ComponentScan

    @Configuration类不仅可以使用组件扫描引导,还可以自己使用@ComponentScan注释配置组件扫描:

    @Configuration
       @ComponentScan("com.acme.app.services")
       public class AppConfig {
           // various @Bean definitions ...
       }
    

    @Import

    @Configuration类可以使用@Import注释组合,因为@Configuration对象在容器中被管理为Spring bean,所以导入的配置可能会被注入——例如,通过构造函数注入:

    @Configuration
    
       public class DatabaseConfig {
      
           @Bean
           public DataSource dataSource() {
               // instantiate, configure and return DataSource
           }
       }
      
       @Configuration
       @Import(DatabaseConfig.class)
       public class AppConfig {
      
           private final DatabaseConfig dataConfig;
      
           public AppConfig(DatabaseConfig dataConfig) {
               this.dataConfig = dataConfig;
           }
      
           @Bean
           public MyBean myBean() {
               // reference the dataSource() bean method
               return new MyBean(dataConfig.dataSource());
           }
       }
    

    @ContextConfiguration

    组合多个@Configuration配置组件

    @RunWith(SpringRunner.class)
       @ContextConfiguration(classes = {AppConfig.class, DatabaseConfig.class})
       public class MyTests {
      
           @Autowired MyBean myBean;
      
           @Autowired DataSource dataSource;
      
           @Test
           public void test() {
               // assertions against myBean ...
           }
       }
    

    @Component

    修饰一个类,表明一个注解的类是“部件”。@Configuration就是基于该注解。

    @Autowired

    自动注入,按照类型来匹配。

    @Profile

    分别开发环境和生产环境的配置

    application.properties 配置文件

    spring.profiles.active=development
    

    测试环境

    @Profile("development")
    @Configuration
      public class EmbeddedDatabaseConfig {
            @Bean
              public DataSource dataSource() {
                  // instantiate, configure and return embedded DataSource
              }
      }
    

    生产环境

      @Profile("production")
      @Configuration
      public class ProductionDatabaseConfig {
        @Bean
          public DataSource dataSource() {
              // instantiate, configure and return production DataSource
          }
      }
    

    第二种方法

    @Configuration
      public class ProfileDatabaseConfig {
          //测试环境
          @Bean("dataSource")
          @Profile("development")
          public DataSource embeddedDatabase() { ... }
         //生产环境
          @Bean("dataSource")
          @Profile("production")
          public DataSource productionDatabase() { ... }
      }
    

    @ImportResource

    导入Spring的配置文件,让配置文件里面的内容生效;Spring Boot里面没有Spring的配置文件,我们自己编写的配置文件

    •     例:@ImportResource(locations = {"classpath:beans.xml"})
      
    •     但是SpringBoott推荐给容器中添加组件的方式;推荐使用全注解的方式
      

    @EnableAutoConfiguration

    启用Spring应用程序上下文的自动配置(开启自动配置spring),当你不需要加载某个自定义类时可以在yml文件中:spring.autoconfigure.exclude: XXXX 来排除某个类。

    @ComponentScan

    配置使用组件扫描指令与@ Configuration类。 提供与Spring XML的支持并行context:component-scan元件。它包含了annotation-config这个属性。

    SpringBoot启动过程

    从main中执行SpringApplication.run(GradleTest1Application.class, args);

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

    进入到SpirngApplication类中,然后在run方法中间接调用new SpringApplication的构造方法

    public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
       return run(new Class<?>[] { primarySource }, args);
    }
    
    public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
       return new SpringApplication(primarySources).run(args);
    }
    

    SpringApplication(primarySources):源码开始


    public SpringApplication(Class<?>... primarySources) {
       this(null, primarySources);
    }
    

    从这里开始,有SpringApplication构造方法中嵌套的源码

    public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
       this.resourceLoader = resourceLoader;		//资源加载器,因为上一次调用赋值为null,所以之这里时null
       Assert.notNull(primarySources, "PrimarySources must not be null");	  //断言这里是否传入的SpringBoot的启动器是否为空
       this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));		//将SpringBoot启动器放入一个Set<Class<?>>中
        /*WebApplicationType:Web应用程序可能类型的枚举,里面有三个枚举参数(查看Web是那种类型),deduceFromClasspath()方法中使用CLassUtils的class工具查看对应 的classpath路径下是否有对应的配置,如果有返回对应的Web类型
        注意:源码在本代码块后第一个代码块中
        */
       this.webApplicationType = WebApplicationType.deduceFromClasspath();
        /*加载默认配置类(ApplicationContextInitializer是初始化所有自动配置类的类),扫描所有自动配置jar包中“META-INF/spring.factories”的配置类jar包(
        	主要有org.springframework.boot.autoconfigure、org.springframework.boot.context、org.springframework.boot.beans)
           找到实现ApplicationContextInitializer接口的类,并实例化他们
           提示:源码在本代码块后第二个代码块中
           */
       setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
        //和上一行代码操作一致,只不过是在三个jar包中找到实现ApplicationListener.class类并实例化它们
       setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
        //找到主要的应用程序类
       this.mainApplicationClass = deduceMainApplicationClass();
        /**
        deduceMainApplicationClass();源码说明:
        	private Class<?> deduceMainApplicationClass() {
    		try {
    			//直接new出一个运行异常,然后获得堆信息
    			StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
    			//在堆信息中找到main方法
    			for (StackTraceElement stackTraceElement : stackTrace) {
    				if ("main".equals(stackTraceElement.getMethodName())) {
    					//加载main方法,并返回class对象
    					return Class.forName(stackTraceElement.getClassName());
    				}
    			}
    		}
    		catch (ClassNotFoundException ex) {
    			// Swallow and continue
    		}
    		return null;
    	}
        */
    }
    

    this.webApplicationType = WebApplicationType.deduceFromClasspath();源码:

    WebApplicationType:Web应用程序可能类型的枚举,里面有三个枚举参数(查看Web是那种类型),deduceFromClasspath()方法中使用CLassUtils的class工具查看对应 的classpath路径下是否有对应的配置,如果有返回对应的Web类型

    /**
     * An enumeration of possible types of web application.
     *
     * @author Andy Wilkinson
     * @author Brian Clozel
     * @since 2.0.0
     */
    public enum WebApplicationType {
    
    	/**
    	 * The application should not run as a web application and should not start an
    	 * embedded web server.
    	 应用程序不应作为web应用程序运行,也不应该启动嵌入式web服务器。
    	 */
    	NONE,
    
    	/**
    	 * The application should run as a servlet-based web application and should start an
    	 * embedded servlet web server.
    	 应用程序应作为基于servlet的web应用程序运行,并应启动嵌入式servlet web服务器。
    	 */
    	SERVLET,
    
    	/**
    	 * The application should run as a reactive web application and should start an
    	 * embedded reactive web server.
    	 应用程序应该作为一个反应性web应用程序运行,并启动一个嵌入式反应性web服务器。当前反应性web服务器在Spring5引进,它可以支持servlet容器,也可以支持servlet职位的容器
    	 */
    	REACTIVE;
        
        
        static WebApplicationType deduceFromClasspath() {
            //判断类路径是否存在,存在:true    不存在:false
           if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
                 && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
              return WebApplicationType.REACTIVE;
           }
           for (String className : SERVLET_INDICATOR_CLASSES) {
              if (!ClassUtils.isPresent(className, null)) {
                 return WebApplicationType.NONE;
              }
           }
           return WebApplicationType.SERVLET;
        }
    }
    

    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); 源码:

    private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
       return getSpringFactoriesInstances(type, new Class<?>[] {});
    }
    
    /**
    重点!!!!!!!!!
    这个类是返回在org.springframework.boot.autoconfigure、org.springframework.boot.context、org.springframework.boot.beans这三个包中继承type类型的所有类,
    这个类在SpringApplication中经常使用!!!!!!!
    */
    private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
        //获得类加载器
       ClassLoader classLoader = getClassLoader();
    
        //获得所有的自动配置类的路径信息     
       Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
           /*     SpringFactoriesLoader.loadFactoryNames(type, classLoader)说明和源码
           |															|										        	|		
           使用set集合为了确保惟一以防止重复,因为这里存放的是所有自动配置类(AutoConfiguration)
           loadFactoryNames(type, classLoader));    
          参数说明: (type = ApplicationContextInitializer.class(记录被加载类的配置信息,并初始化)         classLoader = 系统加载器)
           
           方法调用了loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader)的同名方法,
           loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader)源码:
           
           public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
                String factoryTypeName = factoryType.getName();
                //loadSpringFactories(classLoader):找到所有“META-INF/spring.factories”配置文件中的类
                //getOrDefault(factoryTypeName, Collections.emptyList());   过滤所有的类找出实现factoryTypeName的类并放入集合中		   				  (factoryTypeName=ApplicationContextInitializer.class
                return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
    		}
    		在return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());中获得所有的自动配置类的路径信息     
           */
        
        
        //在获得所有需要初始化的自动配置类后,初始化他们
       List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
        //对类排序
       AnnotationAwareOrderComparator.sort(instances);
        //返回所有排序后的自动配置类集合
       return instances;
    }
    

    到这里结束


    run(args):源码开始


    /**
     * Run the Spring application, creating and refreshing a new
     运行Spring应用程序,创建并刷新一个新应用程序
     * {@link ApplicationContext}.
     * @param args the application arguments (usually passed from a Java main method)
     * @return a running {@link ApplicationContext}
     */
    public ConfigurableApplicationContext run(String... args) {
        //计时器(Spring创始人2001年写的)
       StopWatch stopWatch = new StopWatch();
        //启动计时器
       stopWatch.start();
        //ApplicationContext子类,封装了配置和生命周期,防止ApplicationContext客户端代码看到它们
       ConfigurableApplicationContext context = null;
        //存放SpringApplication启动运行时的异常
       Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
        //以服务器模式运行,在系统可能缺少显示设备、键盘或鼠标这些外设的情况下可以使用该模式,激活该模式:System.setProperty("java.awt.headless", "true");
       configureHeadlessProperty();
        /*SpringApplication运行时的监视器,每运行一次都会创建一个SpringApplicationRunListener的子类放入SpringApplicationRunListeners类中的List<SpringApplicationRunListener> 的集合中*/
       SpringApplicationRunListeners listeners = getRunListeners(args);
        /**     |                                              |											|			源码:
        	private SpringApplicationRunListeners getRunListeners(String[] args) {
                Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
                return new SpringApplicationRunListeners(logger,
                        getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));	//找到继承SpringApplicationRunListener类的所有类
    		}
    	    */
        
        
        //开始启动所有继承SpringApplicationRunListener的监视器
       listeners.starting();
       try {
           //加载默认参数,只是把args赋值给DefaultApplicationArguments类中的成员变量
          ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
           //配置环境,传入所有监听器和应用程序的参数
          ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
           //配置忽略一些Bean信息
          configureIgnoreBeanInfo(environment);
           //向控制台打印Banner(就是那个大的Spring字体,我们可以在classpath目录下创建一个banner.txt文件,把想要打印的东西放入进去就可以在程序启动时打印出来)
          Banner printedBanner = printBanner(environment);
           //创建应用上下文
          context = createApplicationContext();
           //获取Spring工厂的实例
          exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
                new Class[] { ConfigurableApplicationContext.class }, context);
           //准备上下文,并输出某些日志
          prepareContext(context, environment, listeners, applicationArguments, printedBanner);
           //刷新上下文,并输出某个日志
          refreshContext(context);
           //刷新上下问后置处理
          afterRefresh(context, applicationArguments);
           //计时停止了
          stopWatch.stop();
          if (this.logStartupInfo) {
             new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
          }
          listeners.started(context);
          callRunners(context, applicationArguments);
       }
       catch (Throwable ex) {
          handleRunFailure(context, ex, exceptionReporters, listeners);
          throw new IllegalStateException(ex);
       }
    
       try {
          listeners.running(context);
       }
       catch (Throwable ex) {
          handleRunFailure(context, ex, exceptionReporters, null);
          throw new IllegalStateException(ex);
       }
       return context;
    }
    
  • 相关阅读:
    超级简单的分屏控件【自定义PictureBox 继承于UserControl】
    《(学习笔记)两天进步一点点》(3)——应用BindingSource实现数据同步
    《(学习笔记)两天进步一点点》(5)——几个比较小的类,但很实用
    【唠叨两句】如何将一张树型结构的Excel表格中的数据导入到多张数据库表中
    《(学习笔记)两天进步一点点》(2) ——BindingSource基础操作
    微软通用类库——DbHelper
    经典的SQL语句
    ToString 中的常见格式
    【学习笔记】SQL Server 中的批量复制操作 (ADO.NET)
    《(学习笔记)两天进步一点点》(1)——Windows控件DGV
  • 原文地址:https://www.cnblogs.com/baroque/p/12990138.html
Copyright © 2011-2022 走看看