Project-driven learning, practice to test knowledge

preface

At present, the technology stack used for Java backend development is basically relatively uniform: Spring + SpringMVC + Mybatis, which is often referred to as SSM. Although SpringBoot is the popular way to quickly set up and configure SSM projects, it is important to know how to combine all three without SpringBoot, because SpringBoot helps us to do a lot of configuration, not discard it. So knowing how the native SSM is integrated will help us better understand the SSM and the benefits of SpringBoot! And some of the older projects just don’t use SpringBoot, which can be extremely difficult to maintain if you don’t know anything about native SSM integration and configuration.

SSM integration is tedious compared to SpringBoot’s quick setup, but don’t worry, this article will show you how to integrate all three and explain the implications of each configuration step by step. At the end of the article, there is also a mind map and Github address of the project, which can be cloned and run directly. If you want to do a complete SSM project by yourself, you can use this shelf to develop directly.

integration

Project structures,

Create a project

Idea is used for project creation and Maven manages dependency packages. First we create a new project on IDEA, select Maven, and then select Web application:

Click Next, enter GroupId and ArtifactId, and click Next until done. After the project is created, the structure of the whole project is as follows:

Configuring the Web Project

Don’t worry about configuring SSM just yet, we need to configure the Web project under this idea first. As you can see, after the project is set up, the web.xml file says version 2.3, which is too old to work.

We press CATL + Shift + Alt + S to open the Project Structure of idea, then click Modules on the left, then click Web, then click delete button on the right, ok, and finally click APPLY to delete the default:

At this point we notice that the default web.xml file has been deleted. Then click the Add button on the right and click web.xml to add:

Here, we choose version 3.1, and click OK in the pop-up box after selection, and then click OK below to complete the creation:

Once created, we’ll see that our web.xml content is 3.1.

Establish project structure

At this point, don’t worry about configuring SSM. We don’t even have a basic structure of the project. We need to establish where your core code is written, where your tests are, and where your resources are put.

First, we create the test folder under the SRC path, and then the Java and Resources folders under the SRC /main path. Once you have created the folder, right click on the folder, then drag it down, select Mark Directory as, and then select the corresponding Directory structure.

The Java folder corresponds to SourcesRoot, which represents the path marked as the project source code, where the code is written.

The Resources folder corresponds to ResourcesRoot, which is marked as the resource path where all resources such as configuration files are located.

The test folder corresponds to TestSourcesRoot, which is marked as the test path where the test code will be stored.

Once the folder is specified, we will create our code package structure under the Java folder. Packages are divided into the most basic controller, Service, mapper and Entity. The directory structure is as follows:

Import required dependency packages

With the basic project structure in place, it’s time to integrate the SSM. The first thing you should definitely do is import the necessary dependency packages into the pom.xml file, just copy and paste them, with annotations everywhere:

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>

    <! -- Config jar package version -->
    <mysql.version>5.1.48</mysql.version>
    <spring.version>5.2.0. RELEASE</spring.version>
    <jackson.version>2.10.0</jackson.version>
</properties>

<dependencies>
    <! -- Unit testing, note that version 4.12 or later. Scope stands for test and is used only for testing. This dependency package is not distributed with the version package.
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>

    <! -- Logback -->
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.2.3</version>
    </dependency>

    <! Lombok, a tool to simplify getters and setters. Optional: To use Lombok, download the corresponding plugin on IDEA.
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.10</version>
        <scope>provided</scope>
    </dependency>


    <! -- *************** Database configuration ****************** -->
    <! Mysql driver dependencies -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql.version}</version>
    </dependency>

    <! Data source dependencies, which can greatly improve usability and convenience. Here we use Ali's Druid data source -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.12</version>
    </dependency>

    <! Mybatis -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.4.6</version>
    </dependency>

    <! -- *************** Web configuration ****************** -->
    <! JavaEE dependencies include Servlet, Validation, etc.
    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-api</artifactId>
        <version>8.0</version>
        <scope>provided</scope>
    </dependency>

    <! -- JSTL dependencies, which are required if you use JSTL tags in JSPS. Not necessary -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
        <version>1.2</version>
    </dependency>

    <! -- Jackson dependency package, used to convert Java objects to JSON format, used by SpringMVC -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>${jackson.version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>${jackson.version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
        <version>${jackson.version}</version>
    </dependency>

    <! - * * * * * * * * * * * * * * * Spring related configuration * * * * * * * * * * * * * * * * * * -- >
    <! Jar package to configure Spring JDBC container -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>${spring.version}</version>
    </dependency>

    <! The jar package needed to configure the Spring IOC container
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>

    <! --Spring mvc-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>${spring.version}</version>
    </dependency>

    <! AspectJ jar package -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.4</version>
    </dependency>

    <! --Spring test dependencies -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>${spring.version}</version>
        <scope>test</scope>
    </dependency>

    <! Mybatis jar package -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>1.3.2</version>
    </dependency>
</dependencies>
Copy the code

Spring integration with Mybatis

Database Configuration

Before we integrate, let’s prepare the database for a full demonstration. I use MySQL 5.7.25 here, let’s create a database, called ssm_demo executing statements to build a user list and insert two test data:

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'primary key id',
  `name` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT 'Account Name',
  `password` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT 'Account password'.PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

INSERT INTO `user` VALUES (1.'admin'.'123456');
INSERT INTO `user` VALUES (2.'rudecrab'.'654321');
Copy the code

Create an entity class User corresponding to the database table under entity package:

@Data // Lombok annotations automatically generate getters, setters, and toString methods
public class User implements Serializable {
    private Long id;

    private String name;

    private String password;
}
Copy the code

Mysql > create database.properties file in resources folder to configure database connection information.

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=JDBC: mysql: / / 127.0.0.1:3306 / ssm_demo? characterEncoding=utf-8&useSSL=false&autoReconnect=true&rewriteBatchedStatements=true&serverTimezone=UTC
jdbc.username=root
jdbc.password=root
Copy the code

Logback Log configuration

In real projects, log output is usually observed, so let’s configure the log. Create a new logback.xml file in the Resources directory. Note that you need to specify a package at the end of the package. This is set according to the package name of your project structure:


      
<configuration>
    <! Define log file output address -->
    <property name="LOG_ERROR_HOME" value="error"/>
    <property name="LOG_INFO_HOME" value="info"/>

    <! The appender tag defines three log collection strategies: console output, general information file output, and error file output.
    <! The name attribute specifies the name of the appender.
    <! The class attribute specifies the output strategy. There are usually two types, console output and file output. File output is to persist the log.

    <! -- Console output -->
    <appender name="CONSOLE_LOG" class="ch.qos.logback.core.ConsoleAppender">
        <! Use the tag under this tag to specify the log output format -->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <! -- %p: output priority, Namely the DEBUG, INFO, WARN, ERROR, FATAL % r: output from the application start to output the log information amount of milliseconds % t: output produced the log event thread name % f: output log message belongs to the category of category name % c: output the full name of the log messages belonging to the class %d: indicates the date or time of the log outputting time. The format is specified: %d{YYYY-MM-DD HH: MM :ss} % L: indicates the location of the log outputting event, that is, the line of the log outputting statement. %m: prints the message specified in the code, such as message %n in log(message) : prints a newline symbol -->
            <pattern>%red(%d{yyyy-MM-dd HH:mm:ss.SSS}) %yellow([%-5p]) %highlight([%t]) %boldMagenta([%C]) %green([%L]) %m%n</pattern>
        </encoder>
    </appender>

    <! -- General information file output -->
    <appender name="INFO_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <! -- Specify filtering policy by using this tag -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <! -- Tag specifies the type of filtering -->
            <level>ERROR</level>
            <onMatch>DENY</onMatch>
            <onMismatch>ACCEPT</onMismatch>
        </filter>

        <encoder>
            <! -- tag specifies log output format -->
            <pattern>[%d{yyyy-MM-dd' 'HH:mm:ss.SSS}] [%C] [%t] [%L] [%-5p] %m%n</pattern>
        </encoder>

        <! -- Tag specifies a collection policy, such as time-based collection -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <! The tag specifies the location where the generated logs are stored. This configuration has achieved the goal of collecting logs by day.
            <fileNamePattern>${LOG_INFO_HOME}//%d.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
    </appender>

    <! Error message file output -->
    <appender name="ERROR_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
        <encoder>
            <pattern>[%d{yyyy-MM-dd' 'HH:mm:ss.SSS}] [%C] [%t] [%L] [%-5p] %m%n</pattern>
        </encoder>

        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_ERROR_HOME}//%d.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
    </appender>

    <! Set the log level for a package or a class -->
    <logger name="com.rudecrab.ssm.mapper" level="DEBUG"/>

    <! -- Mandatory tag to specify the most basic log output level -->
    <root level="info">
        <! - add append -- -- >
        <appender-ref ref="CONSOLE_LOG"/>
        <appender-ref ref="INFO_LOG"/>
        <appender-ref ref="ERROR_LOG"/>
    </root>
</configuration>
Copy the code

Mybatis global Settings

Now we are finally ready to integrate Spring and Mybatis. Mybatis = mybatis = mybatis = mybatis = mybatis = mybatis = mybatis = mybatis = mybatis = mybatis


      
<! DOCTYPEconfiguration PUBLIC "- / / mybatis.org//DTD Config / 3.0 / EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <! -- Configure global Settings -->
    <settings>
        <! -- Enable logging and specify logging implementation -->
        <setting name="logImpl" value="SLF4J"/>

        <! Enable primary key generation policy -->
        <setting name="useGeneratedKeys" value="true"/>

        <! -- Enable underline to hump mapping -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>

        <! -- Enable level 2 cache -->
        <setting name="cacheEnabled" value="true"/>
    </settings>
</configuration>
Copy the code

Spring-myabtis integrated configuration

The spring-mybatis. XML file is used for integration. Note that many of these Settings need to specify a package, which is based on the package name of the project structure. The comments are clear:


      
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <! Read the properties file, here we read the database connection related configuration -->
    <context:property-placeholder location="classpath:database.properties" file-encoding="UTF-8"/>

    <! -- Configure automatic scanning, if you don't configure this then you can't load beans with @autowired -->
    <context:component-scan base-package="com.rudecrab.ssm" use-default-filters="true">
        <! Controller is only available for MVC to scan, so there is no conflict.
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

    <! -- Configure data source -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <! -- Configure basic JDBC properties, that is, database connection configuration -->
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>

        <! -- Configure the connection pool Settings, this is to be configured according to the actual situation of the project, as the development of the project will change -->
        <property name="initialSize" value="10"/>
        <property name="maxActive" value="100"/>
    </bean>

    <! MyBatis = MyBatis = MyBatis = MyBatis = MyBatis = MyBatis = MyBatis = MyBatis = MyBatis = MyBatis = MyBatis = MyBatis = MyBatis = MyBatis The mapper interface can be accessed via Spring IoC.
    <bean class="org.mybatis.spring.SqlSessionFactoryBean" id="sqlSessionFactory">
        <! -- Specify data source -->
        <property name="dataSource" ref="dataSource"/>
        <! -- Load mybatis global Settings, classpath = resources-->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <! Configure Mybatis mapping XML file path -->
        <property name="mapperLocations" value="classpath:mapper/*.xml"/>
    </bean>

    <! Mybatis mapper interface scan package -->
    <! -- Attention!! If you are using the mapper interface generated automatically by Tk.mybatis, be sure to use org.mybatis. To tk. Mybatis -- -- >
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <! SqlSessionFactory = sqlSessionFactory
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
        <! Mapper interface scan packet -->
        <property name="basePackage" value="com.rudecrab.ssm.mapper"/>
    </bean>

    <! -- Configure the transaction manager, if you do not configure this, do not start the transaction scan, then the exception will not trigger the rollback -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <! You can also specify the data source.
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <! -- Start transaction scan -->
    <tx:annotation-driven/>
</beans>
Copy the code

JUnit tests

Now that Spring and Myabtis are integrated and configured, we’ll have to test it out. Before testing, we need to establish mapper interface file, Myabtis mapping XML file, Service interface and implementation class:

The UserMapper interface is specifically used to declare various database operations, which the @repository annotation defines as spring-managed beans:

@Repository
public interface UserMapper {
    /** * query all User objects * from database@returnThe User object collection */
    List<User> selectAll(a);
}
Copy the code

The usermapper. XML mapping file is used to write the method corresponding to the SQL statement to be executed:


      
<! DOCTYPEmapper PUBLIC "- / / mybatis.org//DTD Mapper / 3.0 / EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.rudecrab.ssm.mapper.UserMapper">
    <! -- Enable cache -->
    <cache/>

    <! SQL > select * from User; select * from User;
    <select id="selectAll" resultType="com.rudecrab.ssm.entity.User">
        select * from user
    </select>

</mapper>
Copy the code

The UserService interface is used to declare business methods about User:

public interface UserService {
    /** * query all User objects * from database@returnThe User object collection */
    List<User> getAll(a);
}
Copy the code

The UserServiceImpl entity class implements the business logic about the User, and the @Service annotation, like the @Repository annotation, defines it as a Bean. The @Transactional annotation is a declarative transaction that can be rolled back if an exception is thrown by a method of that business layer. Then use the @AutoWired annotation to automatically load the Bean on the private property without us having to manually create a UserMapper:

@Service
@Transactional(rollbackFor = Exception.class)
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public List<User> getAll(a) {
        returnuserMapper.selectAll(); }}Copy the code

Now that we have all the relevant classes and files set up, we will create a test class named UserServiceTest in the test folder. We must add the two annotations to the test class, otherwise we will not be able to use Spring functions properly:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:spring-mybatis.xml"})
public class UserServiceTest {
    @Autowired
    private UserService userService;

    @Test
    public void getAll(a) { System.out.println(userService.getAll()); System.out.println(userService.getAll()); }}Copy the code

After the run, we can see the result:

You can see that the results display normally and the log is printed on the console. This means that we have completed the integration of Spring and Mybatis!

Train of thought

SpringMVC

spring-mvc.xml

Next we configure SpringMVC by creating a new spring-mvC.xml file in the Resources directory. The package name is set according to the package name of the project structure:


      
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <! Configure the view resolver so that the controller returns the file name directly.
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <! -- prefix -- -- >
        <property name="prefix" value="/WEB-INF/views/"/>
        <! - the suffix - >
        <property name="suffix" value=".jsp"/>
    </bean>

    <! Configure static resource filtering, otherwise static resources such as CSS cannot be accessed.
    <mvc:default-servlet-handler/>

    <! -- Configure scanned packets -->
    <context:component-scan base-package="com.rudecrab.ssm.controller" use-default-filters="false">
        <! -- Only controller is scanned, it is better to write in this way in actual development, MVC will only scan controller, there is no conflict on the IOC side, otherwise the transaction will be overwritten, IOC side will exclude the controller-->
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

    <! -- Enable MVC annotations -->
    <mvc:annotation-driven/>
</beans>
Copy the code

web.xml

The final configuration, of course, is the integration in web.xml. There are three main configurations:

  1. Configure the Spring IOC container in preparation for Mybatis
  2. Configure the front-end controller for SpringMVC
  3. Configure a character encoding filter

      
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

    <! Mybatis will not be available in the Web application if the Spring IOC container is not configured.
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <! Spring and Mybatis integrate config file path
        <param-value>classpath:spring-mybatis.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <! --2. Configure SpringMVC front-end controller -->
    <servlet>
        <servlet-name>SpringMVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <! --SpringMVC integration configuration file path -->
            <param-value>classpath:spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>SpringMVC</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <! --3. Configure character encoding filter -->
    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/ *</url-pattern>
    </filter-mapping>
</web-app>
Copy the code

The ultimate test

Now that all the configuration is done, let’s run a simple Web project to see if the integration is successful! Remember that we configured the view resolution prefix in the spring-mvC.xml file. We created a new index.jsp file in the **/WEB-INF/views/** folder:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html; charset=UTF-8" language="java"RudeCrab</title> </head> <body> <%-- loop to extract elements from userList --%> <c:forEachvar="user" items="${userList}">
    <ul>
        <li>${user}</li>
    </ul>
</c:forEach>
</body>
</html>
Copy the code

Next, create a new controller class under the Controller package to define the access interface:

@Controller
@RequestMapping(value = "user")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/getList")
    public String getList(Model model) {
        // Store the data in the Model object so that the JSP can access the data
        model.addAttribute("userList", userService.getAll());
        // Returns the JSP file name
        return "index";
    }

    @GetMapping("/getJson")
    @ResponseBody
    public List<User> getList(a) {
        // We can use @responseBody annotation to return the data object directly, so that the front-end can render the data by retrieving JSON
        returnuserService.getAll(); }}Copy the code

Then we start Tomcat and access the interface in the browser:

You can see that we have successfully accessed the data, and now the SSM is fully integrated!

conclusion

Mind mapping

The overall integration and configuration roadmap has been drawn, and detailed remarks have been written on each node, which can be downloaded and viewed. The file is in the last github address:

Making the address

Github.com/RudeCrab/ru…

The above contains the whole project, clone down with IDEA can be opened to run! I also put up a mind map file. If it is helpful to you, please click a STAR, there are other [project practices] in the project, and I will update more project practices in the future!

Blog, Github, wechat public account is: RudeCrab, welcome to follow! If it is helpful to you, you can collect, like, star, look at, share ~~ your support, is the biggest power of my writing

Wechat reprint please contact the public number to open the white list, other places reprint please indicate the original address, the original author!