Я должен сказать, что SpringBoot слишком сложен, я просто хотел изучить, как простейшая программа HelloWorld на SpringBoot запускается шаг за шагом из основного метода, но это довольно глубокая яма. Вы можете попробовать спуститься по коду стека вызовов слой за слоем, если вы не сломаете точку, вы понятия не имеете, куда программа будет течь дальше. Это отличается от языковых фреймворков Go и Python, которые я изучал в прошлом.Они обычно очень просты, дизайн ясен и понятен, код прост для написания, и реализация внутри также очень проста. Но SpringBoot — нет.Внешне он легкий и простой, но внутри он подобен огромному монстру.У этого монстра тысячи ног, запутавшихся в самом себе, что приводит в замешательство читателей, любящих изучать исходный код. Но в мире программирования на Java SpringBoot — старший брат, и вы должны смириться с этим. Даже если в вашем сердце бегут тысячи травяных и грязных лошадей, оно номер один в мире. Если вы академический программист, вы будете сомневаться в своей жизни, когда увидите это явление, вы должны принять правило - самый популярный на рынке не обязательно лучший дизайн, слишком много других иррациональных факторов.
После мучительных пыток я все-таки разобрался с принципом работы SpringBoot и поделюсь им с вами здесь.
Hello World
Во-первых, давайте взглянем на простой код Hello World SpringBoot.Есть только два файла HelloControll.java и Application.java.Запуск Application.java может запустить простой веб-сервер RESTFul.
// HelloController.java
package hello;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
@RestController
public class HelloController {
@RequestMapping("/")
public String index() {
return "Greetings from Spring Boot!";
}
}
// Application.java
package hello;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Когда я открыл браузер и увидел, что сервер нормально рендерит вывод в браузер, я не мог не воскликнуть — SpringBoot чертовски прост.
Но тут возникает вопрос, в основном методе Application у меня вообще нет ссылки на класс HelloController, так как же сервер вызывает его код? Для этого нужно углубиться в метод SpringApplication.run(), чтобы увидеть, что происходит. Но даже если мы не смотрим на код, мы можем легко догадаться, что SpringBoot должен был где-то отсканировать текущий пакет и автоматически зарегистрировать класс, аннотированный RestController, в качестве контроллера уровня MVC в Tomcat Server.
Еще неприятно то, что SpringBoot слишком медленно запускается, запуск простого Hello World занимает до 5 секунд, сложно представить более сложный проект с такой скоростью запуска.
Пожалуйтесь еще раз, хотя этот простой HelloWorld имеет только одну зависимость maven, настроенную в pom, она зависит в общей сложности от 36 пакетов jar, включая 15 пакетов jar, начиная с spring. Сказать, что это ад зависимости, значит ничего не сказать.
Критика почти наступила, дальше нужно войти в тему и посмотреть, как работает основной метод SpringBoot.
Стек SpringBoot
Самый простой способ понять, что делает SpringBoot, — это посмотреть на его стек вызовов, стек вызовов загрузки ниже не слишком глубок, мне не на что жаловаться.
public class TomcatServer {
@Override
public void start() throws WebServerException {
...
}
}
Затем посмотрите на стек времени выполнения, чтобы увидеть, насколько глубок стек вызовов HTTP-запроса. Я не знал, если бы не видел, я был в шоке, когда увидел!
Сделав окно IDE полноэкранным и свернув исходные окна других окон консоли, мне удалось с трудом уместить весь стек вызовов на одном экране.Но, если подумать, это не вина SpringBoot.Большинство из них - это стек вызовов Tomcat, и меньше 10 слоев, связанных со SpringBoot.
Исследуйте загрузчики классов
Еще одна особенность SpringBoot заключается в том, что он использует технологию FatJar для объединения всех зависимых пакетов jar в каталог BOOT-INF/lib в окончательном пакете jar, а классы текущего проекта объединяются в каталог BOOT-INF/classes.
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
Это отличается от подключаемого модуля maven shadow, который мы обычно используем, который распаковывает все файлы классов в зависимом пакете jar, а затем плотно помещает их в унифицированный пакет jar. Давайте разархивируем пакет jar, упакованный Springboot, чтобы увидеть его структуру каталогов.
├── BOOT-INF
│ ├── classes
│ │ └── hello
│ └── lib
│ ├── classmate-1.3.4.jar
│ ├── hibernate-validator-6.0.12.Final.jar
│ ├── jackson-annotations-2.9.0.jar
│ ├── jackson-core-2.9.6.jar
│ ├── jackson-databind-2.9.6.jar
│ ├── jackson-datatype-jdk8-2.9.6.jar
│ ├── jackson-datatype-jsr310-2.9.6.jar
│ ├── jackson-module-parameter-names-2.9.6.jar
│ ├── javax.annotation-api-1.3.2.jar
│ ├── jboss-logging-3.3.2.Final.jar
│ ├── jul-to-slf4j-1.7.25.jar
│ ├── log4j-api-2.10.0.jar
│ ├── log4j-to-slf4j-2.10.0.jar
│ ├── logback-classic-1.2.3.jar
│ ├── logback-core-1.2.3.jar
│ ├── slf4j-api-1.7.25.jar
│ ├── snakeyaml-1.19.jar
│ ├── spring-aop-5.0.9.RELEASE.jar
│ ├── spring-beans-5.0.9.RELEASE.jar
│ ├── spring-boot-2.0.5.RELEASE.jar
│ ├── spring-boot-autoconfigure-2.0.5.RELEASE.jar
│ ├── spring-boot-starter-2.0.5.RELEASE.jar
│ ├── spring-boot-starter-json-2.0.5.RELEASE.jar
│ ├── spring-boot-starter-logging-2.0.5.RELEASE.jar
│ ├── spring-boot-starter-tomcat-2.0.5.RELEASE.jar
│ ├── spring-boot-starter-web-2.0.5.RELEASE.jar
│ ├── spring-context-5.0.9.RELEASE.jar
│ ├── spring-core-5.0.9.RELEASE.jar
│ ├── spring-expression-5.0.9.RELEASE.jar
│ ├── spring-jcl-5.0.9.RELEASE.jar
│ ├── spring-web-5.0.9.RELEASE.jar
│ ├── spring-webmvc-5.0.9.RELEASE.jar
│ ├── tomcat-embed-core-8.5.34.jar
│ ├── tomcat-embed-el-8.5.34.jar
│ ├── tomcat-embed-websocket-8.5.34.jar
│ └── validation-api-2.0.1.Final.jar
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── org.springframework
└── org
└── springframework
└── boot
Преимущество этого метода упаковки заключается в том, что окончательная структура пакета jar очень ясна, и все зависимости ясны с первого взгляда. Если вы используете maven shadow, все файлы классов будут свалены в кучу, и вы не сможете увидеть зависимости. Окончательный сгенерированный пакет jar почти такого же размера.
С точки зрения механизма запуска, использование технологии FatJar для запуска программы требует преобразования пакета jar, а также необходимо настроить свой собственный ClassLoader для загрузки классов в пакете jar, вложенном в каталог lib в пакете jar. Мы можем сравнить файлы МАНИФЕСТА двух, чтобы увидеть очевидную разницу.
// Generated by Maven Shade Plugin
Manifest-Version: 1.0
Implementation-Title: gs-spring-boot
Implementation-Version: 0.1.0
Built-By: qianwp
Implementation-Vendor-Id: org.springframework
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_191
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
ot-starter-parent/gs-spring-boot
Main-Class: hello.Application
// Generated by SpringBootLoader Plugin
Manifest-Version: 1.0
Implementation-Title: gs-spring-boot
Implementation-Version: 0.1.0
Built-By: qianwp
Implementation-Vendor-Id: org.springframework
Spring-Boot-Version: 2.0.5.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: hello.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_191
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
ot-starter-parent/gs-spring-boot
SpringBoot заменил Main-Class в пакете jar на JarLauncher. Также добавляется параметр Start-Class, и класс, соответствующий этому параметру, является реальной записью основного бизнес-метода. Давайте посмотрим, что именно делает этот JarLaucher
public class JarLauncher{
...
static void main(String[] args) {
new JarLauncher().launch(args);
}
protected void launch(String[] args) {
try {
JarFile.registerUrlProtocolHandler();
ClassLoader cl = createClassLoader(getClassPathArchives());
launch(args, getMainClass(), cl);
}
catch (Exception ex) {
ex.printStackTrace();
System.exit(1);
}
}
protected void launch(String[] args, String mcls, ClassLoader cl) {
Runnable runner = createMainMethodRunner(mcls, args, cl);
Thread runnerThread = new Thread(runner);
runnerThread.setContextClassLoader(classLoader);
runnerThread.setName(Thread.currentThread().getName());
runnerThread.start();
}
}
class MainMethodRunner {
@Override
public void run() {
try {
Thread th = Thread.currentThread();
ClassLoader cl = th.getContextClassLoader();
Class<?> mc = cl.loadClass(this.mainClassName);
Method mm = mc.getDeclaredMethod("main", String[].class);
if (mm == null) {
throw new IllegalStateException(this.mainClassName
+ " does not have a main method");
}
mm.invoke(null, new Object[] { this.args });
} catch (Exception ex) {
ex.printStackTrace();
System.exit(1);
}
}
}
Из исходного кода видно, что JarLaucher создает специальный ClassLoader, а затем этот ClassLoader запускает отдельный поток для загрузки MainClass и его запуска.
Возникает еще один вопрос: когда JVM встречает неизвестный класс и в каталоге BOOT-INF/lib так много пакетов jar, как она узнает, какой пакет jar загружать? Давайте продолжим смотреть на исходный код этого конкретного ClassLoader.
class LaunchedURLClassLoader extends URLClassLoader {
...
private Class<?> doLoadClass(String name) {
if (this.rootClassLoader != null) {
return this.rootClassLoader.loadClass(name);
}
findPackage(name);
Class<?> cls = findClass(name);
return cls;
}
}
Здесь rootClassLoader — это ExtensionClassLoader в родительской модели делегирования, и встроенные классы в JVM будут использовать его для загрузки в первую очередь. Если он не встроен, ищите Package, соответствующий этому классу.
private void findPackage(final String name) {
int lastDot = name.lastIndexOf('.');
if (lastDot != -1) {
String packageName = name.substring(0, lastDot);
if (getPackage(packageName) == null) {
try {
definePackage(name, packageName);
} catch (Exception ex) {
// Swallow and continue
}
}
}
}
private final HashMap<String, Package> packages = new HashMap<>();
protected Package getPackage(String name) {
Package pkg;
synchronized (packages) {
pkg = packages.get(name);
}
if (pkg == null) {
if (parent != null) {
pkg = parent.getPackage(name);
} else {
pkg = Package.getSystemPackage(name);
}
if (pkg != null) {
synchronized (packages) {
Package pkg2 = packages.get(name);
if (pkg2 == null) {
packages.put(name, pkg);
} else {
pkg = pkg2;
}
}
}
}
return pkg;
}
private void definePackage(String name, String packageName) {
String path = name.replace('.', '/').concat(".class");
for (URL url : getURLs()) {
try {
if (url.getContent() instanceof JarFile) {
JarFile jf= (JarFile) url.getContent();
if (jf.getJarEntryData(path) != null && jf.getManifest() != null) {
definePackage(packageName, jf.getManifest(), url);
return null;
}
}
} catch (IOException ex) {
// Ignore
}
}
return null;
}
ClassLoader будет локально кэшировать отношение сопоставления между именем пакета и путем к пакету jar.Если соответствующее имя пакета не может быть найдено в кэше, он должен пройти пакет jar один за другим для поиска, что является относительно медленным. Однако одно и то же имя пакета будет найдено только один раз, и в следующий раз соответствующий встроенный путь к пакету jar можно будет получить непосредственно из кэша.
URL-адрес встроенного класса пакета deep jar имеет следующую длину, разделенную восклицательным знаком!
jar:file:/workspace/springboot-demo/target/application.jar!/BOOT-INF/lib/snakeyaml-1.19.jar!/org/yaml/snakeyaml/Yaml.class
Однако этот настраиваемый ClassLoader используется только для среды выполнения упаковки, а в среде разработки IDE метод main напрямую загружается и запускается системным загрузчиком классов.
Я должен сказать, что дизайн SpringbootLoader по-прежнему очень интересен, он очень легкий, а логика кода очень независима и не имеет других зависимостей, что также является одним из достоинств SpringBoot.
HelloController автоматически регистрируется
Остался последний вопрос, то есть на HelloController не ссылается код, как он регистрируется в сервисе Tomcat? Он основан на механизме доставки аннотаций.
SpringBoot глубоко полагается на аннотации для завершения работы по автоматической настройке.Он сам изобрел десятки аннотаций, что серьезно увеличивает умственную нагрузку разработчиков.Вам нужно внимательно прочитать документацию, чтобы знать, для чего она используется. Форма и функции аннотаций Java разделены. Они отличаются от декораторов Python. Они функциональны. Аннотации Java похожи на аннотации кода. У них есть только атрибуты и нет логики. Соответствующие функции аннотаций дополняются разбросанным в других местах кодом. , вам необходимо проанализировать структуру аннотированного класса, чтобы получить атрибуты соответствующей аннотации.Как передаются аннотации?
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@ComponentScan
public @interface SpringBootApplication {
...
}
public @interface ComponentScan {
String[] basePackages() default {};
}
Прежде всего, аннотация, которую можно увидеть в основном методе, — это SpringBootApplication, которая определяется аннотацией ComponentScan.Аннотация ComponentScan определяет имя отсканированного пакета.Если определение не отображается, это текущий путь к пакету. Когда SpringBoot встречает аннотацию ComponentScan, он сканирует все классы в соответствующем пути к пакету и продолжает выполнять последующую обработку в соответствии с другими аннотациями, отмеченными для этих классов. Когда он сканирует класс HelloController, он обнаруживает, что он снабжен аннотацией RestController.
@RestController
public class HelloController {
...
}
@Controller
public @interface RestController {
}
Аннотация RestController снабжена аннотацией Controller. SpringBoot обрабатывает аннотации контроллера особым образом. Он регистрирует классы с аннотациями контроллера как обработчики URL-адресов в обработчике запросов сервлета. Когда создается сервер Tomcat, обработчик запросов передается. Вот как HelloController автоматически подключается к Tomcat.
Сканирование и обработка аннотаций - очень утомительная и грязная работа, особенно этот продвинутый метод использования аннотаций для аннотирования аннотаций (скручивание), этот метод следует использовать экономно и с осторожностью. В SpringBoot очень много кода, связанного с аннотациями, пытаться понять этот код скучно и ненужно, это только запутает вашу изначально трезвую голову. SpringBoot очень удобен для студентов, которые привыкли его использовать, но его внутренний код реализации не должен быть легко имитирован, это определенно не образцовый код Java.
В конце концов, Лао Цянь сказал, что он действительно ненавидит монстра SpringBoot, но он беспомощен, все в мире используют его. Это похоже на то, что старики часто говорят молодым: если вы не можете изменить мир, сначала адаптируйтесь к нему!