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)方法。


使用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;
}

SpringBoot深入理解的更多相关文章

  1. springBoot(1)---springboot初步理解

    springboot初步理解 在没有用SpringBoot之前,我们用spring和springMVC框架,但是你要做很多比如: (1)配置web.xml,加载spring和spring mvc 2) ...

  2. SpringBoot系列: 理解 Spring 的依赖注入(一)

    ==============================Spring 的依赖注入==============================对于 Spring 程序, Spring 框架为我们提供 ...

  3. SpringBoot简单理解

    SpringBoot 一.特点:1.默认大于配置,不需要大量配置文件,没有web.xml,拥有可运行的Application类. 2.一般通过java代码配置,而尽量少使用xml配置. 3.maven ...

  4. 【SpringBoot】 理解Spirng中的IOC原理

    前言 前文已经介绍了Spring Bean的生命周期,在这个周期内有一个重要的概念就是: IOC容器 大家也知道IOC是Sping 的重要核心之一,那么如何理解它呢,它又是产生什么作用呢?本文就IOC ...

  5. 【SpringBoot】 理解SpringBoot的启动原理

    前言 前文已经介绍了Spring Bean的生命周期,那么使用过程中发现SpringBoot 的启动非常快捷,本文将介绍SpringBoot的内部启动原理. 启动过程 如上图所示,我们先分析下Spri ...

  6. SpringBoot系列: 理解 Spring 的依赖注入(二)

    ==============================Spring 容器中 Bean 的名称==============================声明 bean 有两个方式, 一个是 @B ...

  7. SpringBoot对比SpringMVC,SpringMVC 处理请求过程

    (问较多:1.SpringBoot对比SpringMVC.2.SpringMVC 处理请求过程.问:springboot的理解 Spring,Spring MVC,Spring Boot 三者比较 S ...

  8. SSM(SpringMVC+Spring+Mybatis)框架学习理解

    近期做到的项目中,用到的框架是SSM(SpringMVC+Spring+Mybatis).之前比较常见的是SSH.用到了自然得了解各部分的分工 spring mvc 是spring 处理web层请求的 ...

  9. SpringBoot | 第零章:前言

    缘起 前段时间公司领导叫编写一两课关于springboot的基础知识培训课程,说实话,也是今年年初才开始接触了SpringBoot这个脚手架,使用了之后才发现打开了一个新世界.再之后也没有一些系统的学 ...

随机推荐

  1. Excel 单元格快速填充技巧

    1.普通的复制填充空白单元格 直接左键选中单元格右下边框向下拉,选择填充格式(复制填充) 2.普通的顺序填充空白单元格 直接左键选中单元格右下边框向下拉,选择填充格式(序列填充) 3.其他方式填充空白 ...

  2. ADF 第八篇:传递参数(Pipeline的Parameter和Variable,Activity的output)和应用表达式

    Azure Data Factory传递参数的方式主要有两种,通过Pipeline的Parameter和Variable来传递参数,通过Activity的输出来传递参数.要在Activity中引用Pa ...

  3. C#脚本引擎CS-Script

    最近想要在程序中嵌入一个C#脚本引擎,在.NET Framework时代用过一个叫做CS-Script的东西,感觉还是不错,发现现在也支持.NET Core了,试着嵌入一下. 比较 要说能够运行C#脚 ...

  4. 在项目中随手把haseMap改成了currenHaseMap差点被公司给开除了。

    前言 在项目中随手把haseMap改成了currenHaseMap差点被公司给开除了. 判断相等 字符串判断相等 String str1 = null; String str2 = "jav ...

  5. 【转载】VUE的背景图引入

    我现在的项目要将登录页面的背景引一图片做为背景图片,按原jsp中的写法,发现无法找到背景图片,最后从网上查资料,采用上面的写法,成功显示出背景图片,参考网址 https://blog.csdn.net ...

  6. 介绍一个新库: Norns.Urd.HttpClient

    Norns.Urd.HttpClient Norns.Urd.HttpClient 基于AOP框架 Norns.Urd实现, 是对 System.Net.Http下的 HttpClient封装,让大家 ...

  7. Playwright VS Selenium VS Puppeteer VS Cypress

    参考:https://www.testim.io/blog/puppeteer-selenium-playwright-cypress-how-to-choose/ 这四款自动化测试框架在我们的公众号 ...

  8. java interface和class中的协变

    协变 Java中的协变是指,当发生继承时,子类中重写父类的方法时,可以返回父类方法返回类型的子类型.比如: class SuperClass{} class SubClass extends Supe ...

  9. 达梦数据库学习(一、linux操作系统安装及数据库安装)

    达梦数据库学习(一.linux操作系统安装及数据库安装) 环境介绍: 使用VM12+中标麒麟V7.0操作系统+达梦8数据库 一.linux系统搭建 本部分没有需要着重介绍,注意安装时基本环境选择&qu ...

  10. 两个很赞的用法(count函数里的表达式+计算时间间隔)

    1.count函数里写表达式 #无效写法,这样写不会判断表达式(ischecked=0),会全部列出来 SELECT cardid FROM search_detail GROUP BY cardid ...