Implementing the Spring Framework API from scratch. Walkthrough for beginners. Part 1

  • Tutorial


Spring Framework is one of the most complicated frameworks for understanding and learning. Most developers learn it slowly, through practical tasks and google. This approach is not effective, as it does not give a complete picture and at the same time is expensive.

I would like to offer you a fundamentally new approach to the study of Spring. It consists in the fact that a person goes through a series of specially prepared tutorials and independently implements the functional of spring. The peculiarity of this approach is that, in addition to a 100% understanding of the studied aspects of Spring, it also gives a big increase in Java Core (Annotations, Reflection, Files, Generics).

The article will give you an unforgettable experience and make you feel like a Pivotal developer. Step by step, you will make your classes bean and organize their life cycle (the same as in a real spring). The classes you will implement are BeanFactory , Component , Service , BeanPostProcessor , BeanNameAware , BeanFactoryAware , InitializingBean , PostConstruct , PreDestroy , DisposableBean , ApplicationContext , ApplicationListener , ContextClosedEvent .

A little bit about yourself


My name is Yaroslav and I am a Java Developer with 4 years of experience. At the moment I work for EPAM Systems (SPB), and I delve deeply into the technologies that we use. Quite often I have to deal with spring, and I see in it some middle ground in which you can grow (Java everyone knows so well, and too specific tools and technologies can come and go).

A couple of months ago, I passed the Spring Professional v5.0 certification (without taking courses). After that, I thought about how to teach other people springing. Unfortunately, at the moment there is no effective teaching methodology. Most developers have a very superficial idea of ​​the framework and its features. Debuging the spring sources is too difficult and absolutely not effective from the point of view of training (I was somehow fond of this). Do 10 projects? Yes, somewhere you can deepen your knowledge and gain a lot of practical experience, but much of what is “under the hood” will never open before you. Read Spring in Action? Cool, but costly in effort. I've worked it 40% (during preparation for certification), but it was not easy.

The only way to understand something to the end is to develop it yourself. Recently, I had the idea that you can lead a person through an interesting tutorial that will oversee the development of its DI framework. Its main feature will be that the API will coincide with the API being studied. The awesomeness of this approach is that in addition to a deep (without spaces) understanding of spring, a person will get a HUGE amount of experience in Java Core. Frankly, I myself learned a lot of new things during the preparation of the article, both on Spring and on Java Core. Let's get started developing!

Project from scratch


So, the first thing to do is open your favorite IDE and create a project from scratch. We will not connect any Maven or any third-party libraries. We won't even connect Spring dependencies. Our goal is to develop an API that is most similar to the Spring API, and implement it ourselves.

In a clean project, create 2 main packages. The first package is your application ( com.kciray), and the classMain.javainside of it. The second package is org.springframework. Yes, we will duplicate the package structure of the original spring, the name of its classes and their methods. There is such an interesting effect - when you create something of your own, that of your own begins to seem simple and understandable. Then, when you work in large projects, it will seem to you that everything is created there based on your workpiece. This approach can have a very positive effect on understanding the system as a whole, improving it, fixing bugs, solving problems, and so on.

If you have any problems, you can take a working project here .

Create a container


To get started, set the task. Imagine that we have 2 classes - ProductFacadeand PromotionService. Now imagine that you want to connect these classes with each other, but so that the classes themselves do not know about each other (Pattern DI). We need a separate class that will manage all these classes and determine the dependencies between them. Let's call it a container. Create a classContainer... No, wait! Spring does not have a single container class. We have many container implementations, and all these implementations can be divided into 2 types - bin factories and contexts. The bin factory creates beans and links them together (dependency injection, DI), and the context does much the same, plus it adds some additional features (for example, internationalizing messages). But we do not need these additional functions now, so we will work with the bin factory.

Create a new class BeanFactoryand put it in a package org.springframework.beans.factory. Let it be stored inside this class , in which the bean is mapped to the bean itself. Add to it a method that pulls out beans by identifier.Map singletonsidObject getBean(String beanName)

public class BeanFactory {
    private Map singletons = new HashMap();
    public Object getBean(String beanName){
        return singletons.get(beanName);
    }
}

Note that BeanFactoryand FactoryBeanare two different things. The first is the bin factory (container), and the second is the bin factory, which sits inside the container and also produces bins. Factory inside the factory. If you are confused between these definitions, you may remember that in English the second noun is the leading one, and the first is something like an adjective. In Bean Factory, the main word is the factory, and in Factory Bean , the bean.

Now, create classes ProductServiceand PromotionsService.ProductServicewill return the product from the database, but before that you need to check whether any discounts (Promotions) are applicable to this product. In e-commerce, discounted work is often allocated to a separate service class (and sometimes to a third-party web service).

public class PromotionsService {
}
public class ProductService {
    private PromotionsService promotionsService;
    public PromotionsService getPromotionsService() {
        return promotionsService;
    }
    public void setPromotionsService(PromotionsService promotionsService) {
        this.promotionsService = promotionsService;
    }
}

Now we need to make our container ( BeanFactory) detect our classes, create them for us and inject one into the other. Type operations new ProductService()should be located inside the container and done for the developer. Let's use the most modern approach (class scanning and annotations). To do this, we need to create an annotation @Component( пакет org.springframework.beans.factory.stereotype) with pens .

@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
}

By default, annotations are not loaded into memory while the program is running ( RetentionPolicy.CLASS). We changed this behavior through the new retention policy ( RetentionPolicy.RUNTIME).

Now add @Componentbefore classes ProductServiceand before PromotionService.

@Component
public class ProductService {
    //...
}
@Component
public class PromotionService {
    //...
}


We need to BeanFactoryscan our package ( com.kciray) and find classes in it that are annotated @Component. This task is far from trivial. There is no ready-made solution in Java Core , and we will have to make a crutch ourselves. Thousands of spring applications use component scanning through this crutch. You have learned the terrible truth. You will have to extract from ClassLoaderthe file name and check whether they end with ".class" or not, and then build their full name and pull out class objects from it!

I want to warn you right away that there will be many checked exceptions, so be prepared to wrap them. But first, let's decide what we want. We want to add a special method to BeanFactoryand call it in Main:

//BeanFactory.java
public class BeanFactory{
    public void instantiate(String basePackage) {
    }
}
//Main.java
BeanFactory beanFactory = new BeanFactory();
beanFactory.instantiate("com.kciray");

Next, we need to get ClassLoader. It is responsible for loading classes, and it is extracted quite simply:

ClassLoader classLoader = ClassLoader.getSystemClassLoader();

You probably already noticed that the packages are separated by a dot, and the files by a forward slash. We need to convert the batch path to the folder path, and get something like (the paths in your file system by which you can search for class files).List

String path = basePackage.replace('.', '/'); //"com.kciray" -> "com/kciray"
Enumeration resources = classLoader.getResources(path);

So wait a moment! it is not . What is this all about? Oh, horror, this is an old progenitor , available since Java 1.0. This is the legacy we have to deal with. If you can walk through with for (all collections implement it), then in case you have to do a handle bypass, through and . And yet there is no way to remove items from the collection. Only 1996, only hardcore. Oh yes, Java 9 added a method , so you can work through it.EnumerationListIteratorIterable Enumeration while(resources.hasMoreElements())nextElement()Enumeration.asIterator()

Let's go further. We need to extract the folders and work through the contents of each of them. Convert the URL to a file, and then get its name. It should be noted here that we will not scan nested packages so as not to complicate the code. You can complicate your task and make a recursion if you wish.

while (resources.hasMoreElements()) {
    URL resource = resources.nextElement();
    File file = new File(resource.toURI());
    for(File classFile : file.listFiles()){
        String fileName = classFile.getName();//ProductService.class
    }
}

Next, we need to get the file name without the extension. In the yard in 2018, Java has developed File I / O (NIO 2) for many years, but still cannot separate the extension from the file name. I have to create my own bike, because we decided not to use third-party libraries like Apache Commons. Let's use the old grandfather way lastIndexOf("."):

if(fileName.endsWith(".class")){
    String className = fileName.substring(0, fileName.lastIndexOf("."));
}

Next, we can get the class object using the full name of the class (for this we call the class of the class Class):

Class classObject = Class.forName(basePackage + "." + className);

Okay, now our classes are in our hands. Further, it remains only to highlight among them those that have the annotation @Component:

if(classObject.isAnnotationPresent(Component.class)){
    System.out.println("Component: " + classObject);
}

Run and check. The console should be something like this:

Component: class com.kciray.ProductService
Component: class com.kciray.PromotionsService

Now we need to create our bean. It’s necessary to do something like new ProductService(), but for each bin we have our own class. Reflection in Java provides us with a universal solution (the default constructor is called):

Object instance = classObject.newInstance();//=new CustomClass()

Next, we need to put this bean in . To do this, select the bean name (its id). In Java, we call variables like classes (only the first letter is lowercase). This approach can be applied to beans too, because Spring is a Java framework! Convert the bin name so that the first letter is small, and add it to the map:Map singletons

String beanName = className.substring(0, 1).toLowerCase() + className.substring(1);
singletons.put(beanName, instance);

Now make sure everything works. The container must create beans, and they must be retrieved by name. Please note that the name of your method instantiate()and the name of the method classObject.newInstance();have a common root. Moreover, instantiate()it is part of the bin life cycle. In Java, everything is interconnected!

//Main.java
BeanFactory beanFactory = new BeanFactory();
beanFactory.instantiate("com.kciray");
ProductService productService = (ProductService) beanFactory.getBean("productService");
System.out.println(productService);//ProductService@612


Try also implementing annotation org.springframework.beans.factory.stereotype.Service. It performs exactly the same function as @Component, but is called differently. The whole point is in the name - you demonstrate that the class is a service, not just a component. This is something like conceptual typing. In the spring certification there was a question “What annotations are stereotyped?” (of those listed). ” So, stereotypical annotations are those that are in the package stereotype.

Fill the properties


Look at the diagram below, it shows the beginning of the bean's life cycle. What we did before is Instantiate (creating beans through newInstance()). The next step is the cross-injection of beans (dependency injection, it is also the inversion of control (IoC)). You need to go through the properties of the beans and understand which properties you need to inject. If you call now productService.getPromotionsService(), you will receive null, because dependency not yet added.



To get started, create a package org.springframework.beans.factory.annotationand add annotation to it @Autowired. The idea is to flag fields that are dependencies with this annotation.

@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}

Next, add it to the property:

@Component
public class ProductService {
    @Autowired
    PromotionsService promotionsService;
    //...
}

Now we need to teach ours to BeanFactoryfind these annotations and inject dependencies on them. Add a separate method for this, and call it from Main:

public class BeanFactory {
    //...
    public void populateProperties(){
        System.out.println("==populateProperties==");
    }
}

Next, we just need to go through all our bins in the map singletons, and for each bin go through all its fields (the method object.getClass().getDeclaredFields()returns all fields, including private ones). And check if the field has an annotation @Autowired:

for (Object object : singletons.values()) {
    for (Field field : object.getClass().getDeclaredFields()) {
        if (field.isAnnotationPresent(Autowired.class)) {
        }
    }
}

Next, we need to go through all the bins one more time and see their type - suddenly this is the type that our bin wants to take for itself. Yes, we get a three-dimensional cycle!

for (Object dependency : singletons.values()) {
    if (dependency.getClass().equals(field.getType())) {
    }
}

Further, when we found the addiction, we need to inject it. The first thing you might think of is to record the field promotionsServiceusing reflection directly. But spring doesn't work like that. After all, if the field has a modifier private, then we will first have to set it as public, then write our value, then set it again private(to maintain integrity). Sounds like a big crutch. Instead of a large crutch, let's make a small crutch (we will form the name of the setter and call it):

String setterName = "set" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);//setPromotionsService
System.out.println("Setter name = " + setterName);
Method setter = object.getClass().getMethod(setterName, dependency.getClass());
setter.invoke(object, dependency);

Now run your project and make sure that when you call productService.getPromotionsService(), null our bean is returned instead .

What we have implemented is injection by type. There is also an injection by name (abstract javax.annotation.Resource). It differs in that instead of the type of the field, its name will be extracted, and according to it - the dependence from the map. Everything is similar here, even in something simpler. I recommend that you experiment and create some kind of bean of your own, and then inject it with @Resourceand extend the method populateProperties().

We support beans that know about their name




There are times when you need to get his name inside the bin. Such a need does not arise often, because bins, in essence, should not know about each other and that they are bins. In the first versions of the spring, it was assumed that the bean is a POJO (Plain Old Java Objec, the good old Java object), and the entire configuration is rendered in XML files and separated from the implementation. But we implement this functionality, since the name injection is part of the bin's life cycle.

How do we know which bean wants to know what his name is and what he does not want? The first thing that comes to mind is to make a new annotation like@InjectNameand sculpt it into fields of type String. But this solution will be too general and allows you to shoot yourself in the foot many times (place this annotation on fields of inappropriate types (not String), or try to inject a name into several fields in the same class). There is another solution, more accurate - to create a special interface with one setter method. All bins that implement it get their name. Create a class BeanNameAwarein the package org.springframework.beans.factory:

public interface BeanNameAware {
    void setBeanName(String name);
}

Next, let our PromotionsServiceimplement it:

@Component
public class PromotionsService implements BeanNameAware {
    private String beanName;
    @Override
    public void setBeanName(String name) {
        beanName = name;
    }
    public String getBeanName() {
        return beanName;
    }
}

And finally, add a new method to the bean factory. Everything is simple here - we go through our bin-singleton, check if the bin implements our interface, and call the setter:

public void injectBeanNames(){
    for (String name : singletons.keySet()) {
        Object bean = singletons.get(name);
        if(bean instanceof BeanNameAware){
            ((BeanNameAware) bean).setBeanName(name);
        }
    }
}

Run and make sure everything works:

BeanFactory beanFactory = new BeanFactory();
beanFactory.instantiate("com.kciray");
beanFactory.populateProperties();
beanFactory.injectBeanNames();
//...
System.out.println("Bean name = " + promotionsService.getBeanName());

It should be noted that in the spring there are other similar interfaces. I recommend that you implement the BeanFactoryAware interface yourself , which allows beans to receive a link to the bean factory. It is implemented in a similar way.

Initialize Beans




Imagine that you have a situation where you need to execute some code after the dependencies have been injected (bin properties are set). In simple terms, we need to give the bin the ability to initialize itself. Alternatively, we can create an interface InitializingBeanand put the method signature in it void afterPropertiesSet(). The implementation of this mechanism is exactly the same as that presented for the interface BeanNameAware, so the solution is under the spoiler. Practice and do it yourself in a minute:

Bean Initialization Solution
//InitializingBean.java
package org.springframework.beans.factory;
public interface InitializingBean {
    void afterPropertiesSet();
}
//BeanFactory.java
public void initializeBeans(){
    for (Object bean : singletons.values()) {
        if(bean instanceof InitializingBean){
            ((InitializingBean) bean).afterPropertiesSet();
        }
    }
}
//Main.java
beanFactory.initializeBeans();



Adding Post Processors


Imagine yourself in the place of the first spring developers. Your framework is growing and is very popular among developers, letters are sent to the mail every day with requests to add one or another useful feature. If for each such feature you add your own interface and check it in the bean's life cycle, then it (the life cycle) will become clogged with unnecessary information. Instead, we can create one universal interface that allows you to add some logic (absolutely any, whether it is checking for annotation, replacing the bin with another bin, setting some special properties, and so on).

Let's think about what this interface is for. It needs to do some post processing of the beans, hence it can be called BeanPostProcessor. But we are faced with a difficult question - when should logic be followed? After all, we can execute it before initialization, but we can execute it after. For some tasks, the first option is better, for others - the second ... How to be?

We can enable both options at once. Let one post-processor carry two logics, two methods. One is executed before initialization (before the method afterPropertiesSet()), and the other after. Now let's think about the methods themselves - what parameters should they have? Obviously, the bean itself (Object bean) For convenience, in addition to the bean, you can pass the name of this bean. You remember that the bin itself does not know about its name. And we do not want to force all beans to implement the BeanNameAware interface. But, at the post-processor level, the bean name can be very useful. Therefore, we add it as the second parameter.

And what should the method return when post processing the bean? Let's make it return the bin itself. This gives us super-flexibility, because instead of a bin, you can slip a proxy object that wraps its calls (and adds security). Or you can completely return another object by re-creating the bin again. Developers are given very great freedom of action. Below is the final version of the designed interface:

package org.springframework.beans.factory.config;
public interface BeanPostProcessor {
    Object postProcessBeforeInitialization(Object bean, String beanName);
    Object postProcessAfterInitialization(Object bean, String beanName);
}

Next, we need to add a list of simple processors to our bean factory and the ability to add new ones. Yes, this is a regular ArrayList.

//BeanFactory.java
private List postProcessors = new ArrayList<>();
public void addPostProcessor(BeanPostProcessor postProcessor){
    postProcessors.add(postProcessor);
}

Now we change the method initializeBeans so that it takes into account post-processors:

public void initializeBeans() {
    for (String name : singletons.keySet()) {
        Object bean = singletons.get(name);
        for (BeanPostProcessor postProcessor : postProcessors) {
            postProcessor.postProcessBeforeInitialization(bean, name);
        }
        if (bean instanceof InitializingBean) {
            ((InitializingBean) bean).afterPropertiesSet();
        }
        for (BeanPostProcessor postProcessor : postProcessors) {
            postProcessor.postProcessAfterInitialization(bean, name);
        }
    }
}

Let's create a small post processor that simply traces calls to the console and add it to our bean factory:

public class CustomPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        System.out.println("---CustomPostProcessor Before " + beanName);
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        System.out.println("---CustomPostProcessor After " + beanName);
        return bean;
    }
}

//Main.java
BeanFactory beanFactory = new BeanFactory();
beanFactory.addPostProcessor(new CustomPostProcessor());


Now run and make sure everything works. As a training task, create a post processor that will provide annotation @PostConstruct (javax.annotation.PostConstruct). It provides an alternative way to initialize (rooted in Java, not in spring). Its essence is that you place the annotation on some method, and this method will be called BEFORE standard spring initialization (InitializingBean).

Be sure to create all annotations and packages (even javax.annotation) manually, do not connect the dependencies! This will help you to see the difference between the spring core and its extensions (javax support), and remember it. This will keep one style in the future.

You will be interested in the fact that in a real spring, the annotation@PostConstructthis is exactly what is implemented, through the post-processor CommonAnnotationBeanPostProcessor. But do not peek there, write your implementation.

Lastly, I recommend you add a method void close()to the class BeanFactoryand work out two more mechanisms. The first is an annotation @PreDestroy (javax.annotation.PreDestroy), intended for methods that should be called when the container is closed. The second is the interface org.springframework.beans.factory.DisposableBeanthat contains the method void destroy(). All bins executing this interface will have the ability to destroy themselves (free up resources, for example).

@PreDestroy + DisposableBean
//DisposableBean.java
package org.springframework.beans.factory;
public interface DisposableBean {
    void destroy();
}
//PreDestroy.java
package javax.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface PreDestroy {
}
//DisposableBean.java
public void close() {
    for (Object bean : singletons.values()) {
        for (Method method : bean.getClass().getMethods()) {
            if (method.isAnnotationPresent(PreDestroy.class)) {
                try {
                    method.invoke(bean);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
        if (bean instanceof DisposableBean) {
            ((DisposableBean) bean).destroy();
        }
    }
}



Full Bean Lifecycle


So we have implemented the full life cycle of the bin, in its modern form. I hope this approach helps you remember it.

Our favorite context


Programmers very often use the term context, but not everyone understands what it really means. Now we will put everything in order. As I noted at the beginning of the article, context is the implementation of the container, as well as BeanFactory. But, in addition to the basic functions (DI), it still adds some cool features. One of these features is sending and processing events between bins.

The article turned out to be too large and the content began to be cut off, so I put the context information under the spoiler.

We realize the context
Let's start with preparing the context. Create a package org.springframework.context, and a class ApplicationContextinside it. Let it contain an instance of the class BeanFactory. All the initialization steps will be placed in the constructor, and we will also add the method redirection close().

public class ApplicationContext {
    private BeanFactory beanFactory = new BeanFactory();
    public ApplicationContext(String basePackage) throws ReflectiveOperationException{
        System.out.println("******Context is under construction******");
        beanFactory.instantiate(basePackage);
        beanFactory.populateProperties();
        beanFactory.injectBeanNames();
        beanFactory.initializeBeans();
    }
    public void close(){
        beanFactory.close();
    }
}


Добавьте его в класс Main, запустите и убедитесь, что он работает:

ApplicationContext applicationContext = new ApplicationContext("com.kciray");
applicationContext.close();

Теперь давайте подумаем, как организовать события. Поскольку у нас уже есть метод close(), мы можем создать событие «Закрытие контекста» и перехватить его внутри какого-нибудь бина. Создайте простой класс, представляющий данное событие:

package org.springframework.context.event;
public class ContextClosedEvent {
} 

Теперь нам надо создать интерфейс ApplicationListener, который позволит бинам слушать наши события. Поскольку мы решили представлять события в виде классов, то имеет смысл типизировать этот интерфейс по классу события (ApplicationListener). Да, мы будем использовать Java-дженерики, и вы получите немножко опыта по работе с ними. Далее, вам нужно придумать название для метода, который будет обрабатывать событие:

package org.springframework.context;
public interface ApplicationListener{
    void onApplicationEvent(E event);
}

Now back to the class ApplicationContext. In the method, we need to close()go through all of our bins and find out which of them are listeners of events. If the bin is instantiable , then you need to call it . It seems simple and logical, right?ApplicationListeneronApplicationEvent(ContextClosedEvent)

public void close(){
    beanFactory.close();
    for(Object bean : beanFactory.getSingletons().values()) {
        if (bean instanceof ApplicationListener) {
        }
    }
}

But no. This is where the difficulty arises. We CANNOT do a type check . This is due to the peculiarity of the Java implementation. When compiling, a so-called type erasure occurs , in which everythingbean instanceof ApplicationListener replaced by . Как же быть, что же делать? Как нам выловить бины, которые имплементят именно ApplicationListener, а не другие типы событий?

На самом деле, информация о типах сохраняется в метаданных класса, и очень даже успешно извлекается через рефлексию. Для начала, нам нужно получить список из интерфейсов, которые данный бин реализует, и отфильтровать среди них те, которые имеют параметры:

for (Type type: bean.getClass().getGenericInterfaces()){
    if(type instanceof ParameterizedType){
        ParameterizedType parameterizedType = (ParameterizedType) type;
    }
}

Далее, мы всего лишь извлекаем тип первого параметра, и убеждаемся, что он — наш класс события. Если это так, мы получаем наш метод через рефлексию и вызываем его:

Type firstParameter = parameterizedType.getActualTypeArguments()[0];
if(firstParameter.equals(ContextClosedEvent.class)){
    Method method = bean.getClass().getMethod("onApplicationEvent", ContextClosedEvent.class);
    method.invoke(bean, new ContextClosedEvent());
}

Пускай один из ваших классов реализует интерфейс ApplicationListener:

@Service
public class PromotionsService implements BeanNameAware, ApplicationListener {
    //...
    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        System.out.println(">> ContextClosed EVENT");
    }
}

Далее, тестируете ваш контекст в Main и убеждаетесь, что он также работает, и событие отправляется:

//Main.java
void testContext() throws ReflectiveOperationException{
    ApplicationContext applicationContext = new ApplicationContext("com.kciray");
    applicationContext.close();
}


Заключение


Изначально я планировал данную статью для Baeldung на английском, но потом подумал, что аудитория хабры может положительно оценить данный подход к обучению. Если вам понравились мои идеи, обязательно поддержите статью. Если она наберёт рейтинг более 30, то обещаю продолжение. При написании статьи, я старался показать именно те знания Spring Core, которе используются наиболее часто, а также с опорой на Core Spring 5.0 Certification Study Guide. В будущем, с помощью таких туториалов можно покрыть всю сертификацию и сделать спринг более доступным для Java-разработчиков.

Update 10/05/2018


Мне постоянно приходят письма с вопросами «а когда продолжение, мы его ждём». Но вот времени совсем нету, и другие личные проекты в приоритете. Однако, если кому-то из вас действительно понравилась идея, вы можете изучить узкий раздел спринга и написать статью-продолжение. Если у вас нету аккаунта хабры, то я могу опубликовать статью от моего акка или помочь вам получить инвайт.

Распределение тем:
Spring Container — [имя пользователя]
Spring AOP — [имя пользователя]
Spring Web — [имя пользователя]
Spring Cloud — [имя пользователя]

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Тема для следующей статьи

  • 49%Spring Container, углубляемся ещё больше (@Bean, @Configuration, Context, Prototype, XML)81
  • 13.3%Spring AOP, пишем прокси своими руками (@Aspect, @Pointcut)22
  • 29%Spring Web, веб-сервер с нуля (@Contoller, @RequestMapping)48
  • 3%Spring WebFlux5
  • 5.4%Spring Cloud9

Also popular now: