Spring Boot tutorial: how to develop a starter

Keywords: Spring Java SSL Programming

How to develop a custom starter

  • One or more custom configured property configuration classes (optional)
  • One / more autoconfig classes
  • The auto configuration class is written to the SPI mechanism configuration file of Spring Boot: spring.factories

Introduction to Java SPI mechanism

The core of Spring Boot's starter is to automatically inject configuration classes through SPI mechanism, but it's a set of SPI mechanism implemented by itself. Let's learn about Java's SPI mechanism first.

The full name of SPI is Service Provider Interface, which is a set of API s provided by Java to be implemented or extended by a third party. It can be used to enable framework extension and replace components.

The process of SPI is:

Caller – > standard service interface – > local service discovery (configuration file) - > specific implementation

In fact, Java SPI is a dynamic loading mechanism realized by the combination of "interface based programming + policy mode + configuration file".

A simple Java SPI development step:

  • Define a business interface
  • Write interface implementation class
  • Create the SPI configuration file and write the class path to the configuration file
  • Call through Java SPI mechanism

 

 

Bottom implementation of Spring Boot SPI mechanism

After understanding the SPI mechanism of Java, you can basically guess the SPI implementation of Spring Boot. The basic process is the same:

Read configuration file – > assemble specific implementation classes into the Spring Boot context

Next we look for the answer from the source code.

Entry: the startup class of Spring Boot, @ SpringBootApplication annotation. Looking at the source code, you can find that this is a composite annotation, including @ SpringBootConfiguration, @ EnableAutoConfiguration, @ ComponentScan.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
        @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM,
                classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
    ...
}

@EnableAutoConfiguration. Students familiar with Spring Boot should know that Spring Boot has many @ EnableXXX annotations. Its implementation is to import an implementation class of xxxSelector through @ Import(xxxSelector) to load the configuration class:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    ...
}

Continue to see the AutoConfigurationImportSelector source code. We can focus on the selectImports method, which is used to assemble the autoconfiguration class:

@Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        }
        //Load the auto configuration metadata configuration file, which will be explained later
        AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
                .loadMetadata(this.beanClassLoader);
        //Loading the automatic configuration class will merge the configuration classes in the metadata configuration file above    
        AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(
                autoConfigurationMetadata, annotationMetadata);
        return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    }

Continue to track source code:

getAutoConfigurationEntry –> getCandidateConfigurations –>SpringFactoriesLoader.loadFactoryNames –> loadSpringFactories –> classLoader.getResources (FACTORIES_RESOURCE_LOCATION)

Finally, we found the SPI configuration file: facts? Resource? Location.

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

At this point, you can see the loading process of Spring Boot. The configuration classes defined under META-INF/spring.factories will be automatically assembled into the context of Spring Boot.

Develop a custom starter

After understanding the SPI loading mechanism of Spring Boot, we'll develop a custom starter. I'll write a simple email starter here. To simplify the code, I still rely on the mail starter provided by Spring Boot. On this basis, I'll carry out a layer of encapsulation:

1. Create a module: email spring boot starter to introduce dependency.

<!-- Mail sending support -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        <!-- Template mail -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <scope>provided</scope>
        </dependency>

2. Write a mail sending template class. Here I add a switch to enable or not:

@ConditionalOnProperty (name = "dragon.boot.email.enable", havingValue = "true")
@Slf4j
@Configuration
@ConditionalOnProperty(name = "dragon.boot.email.enable", havingValue = "true")
public class MailSenderTemplate {
     //Inject the mail sending class in the mail provided by Spring Boot
    @Autowired
    private JavaMailSender mailSender;
    @Value("${spring.mail.from}")
    private String from;
    @Autowired
    private FreeMarkerConfigurer freeMarkerConfigurer;

    /**
     * @MethodName: send
     * @Author: pengl
     * @Date: 2019-10-31 13:38
     * @Description: Send mail
     * @Version: 1.0
     * @Param: [to, content, subject]
     * @Return: com.dragon.boot.common.model.Result
     **/
    public Result send(String to, String content, String subject) {
        return send(MailDto.builder().to(to).content(content).subject(subject).build());
    }

    /**
     * @MethodName: send
     * @Author: pengl
     * @Date: 2019-10-31 13:39
     * @Description: Send mail (CC)
     * @Version: 1.0
     * @Param: [to, content, subject, cc]
     * @Return: com.dragon.boot.common.model.Result
     **/
    public Result send(String to, String content, String subject, String cc) {
        return send(MailDto.builder().to(to).content(content).subject(subject).cc(cc).build());
    }

    /**
     * @MethodName: sendTemplate
     * @Author: pengl
     * @Date: 2019-10-31 13:39
     * @Description: Send template mail
     * @Version: 1.0
     * @Param: [to, model, template, subject]
     * @Return: com.dragon.boot.common.model.Result
     **/
    public Result sendTemplate(String to, Map<String, Object> model, String template, String subject) {
        return send(MailDto.builder().to(to).content(getTemplateStr(model, template)).subject(subject).build());
    }

    /**
     * @MethodName: sendTemplate
     * @Author: pengl
     * @Date: 2019-10-31 13:39
     * @Description: Send template mail (with CC)
     * @Version: 1.0
     * @Param: [to, model, template, subject, cc]
     * @Return: com.dragon.boot.common.model.Result
     **/
    public Result sendTemplate(String to, Map<String, Object> model, String template, String subject, String cc) {
        return send(MailDto.builder().to(to).content(getTemplateStr(model, template)).subject(subject).cc(cc).build());
    }

    /**
     * @MethodName: getTemplateStr
     * @Author: pengl
     * @Date: 2019-10-31 13:38
     * @Description: Analysis of freemark template
     * @Version: 1.0
     * @Param: [model, template]
     * @Return: java.lang.String
     **/
    private String getTemplateStr(Map<String, Object> model, String template) {
        try {
            return FreeMarkerTemplateUtils.processTemplateIntoString(freeMarkerConfigurer.getConfiguration().getTemplate(template), model);
        } catch (Exception e) {
            log.error("Exception in getting template data:{}", e.getMessage(), e);
        }
        return "";
    }

    /**
     * @MethodName: send
     * @Author: pengl
     * @Date: 2019-10-31 13:34
     * @Description: Send mail
     * @Version: 1.0
     * @Param: [mailDto]
     * @Return: com.dragon.boot.common.model.Result
     **/
    public Result send(MailDto mailDto) {

        if (StringUtils.isAnyBlank(mailDto.getTo(), mailDto.getContent())) {
            return new Result(false, 1001, "Recipient or message content cannot be empty");
        }

        String[] tos = filterEmail(mailDto.getTo().split(","));
        if (tos == null) {
            log.error("Failed to send mail. The format of recipient's email is incorrect:{}", mailDto.getTo());
            return new Result(false, 1002, "");
        }

        MimeMessage mimeMessage = mailSender.createMimeMessage();
        try {
            MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
            helper.setFrom(from);
            helper.setTo(tos);
            helper.setText(mailDto.getContent(), true);
            helper.setSubject(mailDto.getSubject());

            //CC
            String[] ccs = filterEmail(mailDto.getCc().split(","));
            if (ccs != null) {
                helper.setCc(ccs);
            }

            //Secret copy
            String[] bccs = filterEmail(mailDto.getBcc().split(","));
            if (bccs != null) {
                helper.setBcc(bccs);
            }

            //Timing transmission
            if (mailDto.getSendDate() != null) {
                helper.setSentDate(mailDto.getSendDate());
            }

            //Enclosure
            File[] files = mailDto.getFiles();
            if (files != null && files.length > 0) {
                for (File file : files) {
                    helper.addAttachment(file.getName(), file);
                }
            }
            mailSender.send(mimeMessage);
        } catch (Exception e) {
            log.error("Mail sending exception:{}", e.getMessage(), e);
            return new Result(false, 1099, "Mail sending exception:" + e.getMessage());
        }
        return new Result();
    }

    /**
     * Check and filter mailbox format
     *
     * @param emails
     * @return
     */
    private String[] filterEmail(String[] emails) {
        List<String> list = Arrays.asList(emails);
        if (CollectionUtil.isEmpty(list)) {
            return null;
        }

        list = list.stream().filter(e -> checkEmail(e)).collect(Collectors.toList());
        return list.toArray(new String[list.size()]);
    }

    private boolean checkEmail(String email) {
        return ReUtil.isMatch("\\w+@\\w+\\.[a-z]+(\\.[a-z]+)?", email);
    }
}

3. Write the SPI configuration file, create a new folder META-INF under resources, and create the configuration file spring.factories. The content is as follows:

//Replace with your own path
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.dragon.boot.mail.service.MailSenderTemplate 

4. A simple starter module is written. This dependency is introduced when using. Add configuration to the application.properties property file.

 # Mail sending configuration
spring.mail.host=mail.xxx.com
spring.mail.username=xx
 spring.mail.password=xx
spring.mail.protocol=smtp
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.port=465
spring.mail.properties.mail.display.sendmail=xx@qq.com
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.ssl.enable=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
spring.mail.properties.mail.smtp.socketFactory.fallback=false
spring.mail.default-encoding=utf-8
spring.mail.from=xx@qq.com
# Maximum length of all attachments (in bytes, default 100M)
spring.mail.maxUploadSize=104857600
spring.mail.maxInMemorySize=4096

#Enable email module
dragon.boot.email.enable=true

This is just the simplest example. If you strictly follow the specification, you can put all autoconfig classes, including Property property configuration class and logic configuration class, into a separate module, and then start another starter module to introduce this independent autoconfig module.

Custom starter optimization

Automatic prompt function of property configuration: when using the official starter provided by Spring Boot, there is an automatic prompt function to write property configuration in application.properties. To achieve this, it is also very simple to introduce a dependency. After the plug-in is introduced, the class under @ ConfigurationProperties will be checked during packaging, and spring-configuration-metadata.json will be generated automatically The file is used to write attribute prompts:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
</dependency>
  • Start optimization: as mentioned earlier, the SPI loading process of Spring Boot will first load the auto configuration metadata configuration file, introduce the following dependencies, and the plug-in will automatically generate META-INF/spring-autoconfigure-metadata.properties for auto configuration import selector to filter and load, so as to improve the startup performance:
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
</dependency>
Published 2 original articles, praised 0 and visited 5015
Private letter follow

Posted by vimukthi on Wed, 11 Mar 2020 00:11:34 -0700