Article Title Spring Boot Dry Goods Series: (11) Data Storage Chapter - Spring Boot Integrated Mybatis Universal Mapper Plug-in

Keywords: Attribute Spring Mybatis Druid

Preface

Last time I introduced the simple integration of Mybatis in Spring Boot. This article goes deep into creating a template framework suitable for enterprise development by combining generic Mapper, Mybatis Geneator and Page Helper.

text

The project framework still uses Spring Boot's ace back-end template as in the previous article, but recently it uses vue, so the front-end refers to Vue to rewrite and the code becomes more concise.

Project configuration:

Spring Boot: 1.5.9.RELEASE
Maven: 3.5
Java: 1.8
Thymeleaf: 3.0.7.RELEASE
Vue.js: v2.5.11

Data source dependency

Here we still use Alibaba's druid as the database connection pool, and find that there is a corresponding monitoring interface, we can open it.
druid official documents: https://github.com/alibaba/druid/wiki/ Common problem

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.0.19</version>
</dependency>

The corresponding application.properties configuration:

## Database Access Configuration
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf-8
spring.datasource.username = root
spring.datasource.password = root

# Here are additional settings for connection pooling that apply to all of the above data sources
# Initialization size, minimum, maximum
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
# Configuration to get the connection waiting timeout time
spring.datasource.maxWait=60000
# How often is the configuration interval detected to detect idle connections that need to be closed in milliseconds?
spring.datasource.timeBetweenEvictionRunsMillis=60000
# Configure the minimum lifetime of a connection in the pool in milliseconds
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
# Open the PSCache and specify the size of the PSCache on each connection
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
# Configure filters that are intercepted by monitoring statistics, and sql can not be counted after removing them.'wall'is used for firewall
spring.datasource.filters=stat,wall,log4j
# Merge monitoring data from multiple Druid Data Sources
#spring.datasource.useGlobalDataSourceStat=true

The corresponding bean configuration:

package com.dudu.config;

/**
 * Druid To configure
 *
 * @author dudu
 * @date 2017-12-11 0:00
 */
@Configuration
public class DruidConfig {
    private Logger logger = LoggerFactory.getLogger(DruidConfig.class);

    @Value("${spring.datasource.url:#{null}}")
    private String dbUrl;
    @Value("${spring.datasource.username: #{null}}")
    private String username;
    @Value("${spring.datasource.password:#{null}}")
    private String password;
    @Value("${spring.datasource.driverClassName:#{null}}")
    private String driverClassName;
    @Value("${spring.datasource.initialSize:#{null}}")
    private Integer initialSize;
    @Value("${spring.datasource.minIdle:#{null}}")
    private Integer minIdle;
    @Value("${spring.datasource.maxActive:#{null}}")
    private Integer maxActive;
    @Value("${spring.datasource.maxWait:#{null}}")
    private Integer maxWait;
    @Value("${spring.datasource.timeBetweenEvictionRunsMillis:#{null}}")
    private Integer timeBetweenEvictionRunsMillis;
    @Value("${spring.datasource.minEvictableIdleTimeMillis:#{null}}")
    private Integer minEvictableIdleTimeMillis;
    @Value("${spring.datasource.validationQuery:#{null}}")
    private String validationQuery;
    @Value("${spring.datasource.testWhileIdle:#{null}}")
    private Boolean testWhileIdle;
    @Value("${spring.datasource.testOnBorrow:#{null}}")
    private Boolean testOnBorrow;
    @Value("${spring.datasource.testOnReturn:#{null}}")
    private Boolean testOnReturn;
    @Value("${spring.datasource.poolPreparedStatements:#{null}}")
    private Boolean poolPreparedStatements;
    @Value("${spring.datasource.maxPoolPreparedStatementPerConnectionSize:#{null}}")
    private Integer maxPoolPreparedStatementPerConnectionSize;
    @Value("${spring.datasource.filters:#{null}}")
    private String filters;
    @Value("{spring.datasource.connectionProperties:#{null}}")
    private String connectionProperties;

    @Bean
    @Primary
    public DataSource dataSource(){
        DruidDataSource datasource = new DruidDataSource();

        datasource.setUrl(this.dbUrl);
        datasource.setUsername(username);
        datasource.setPassword(password);
        datasource.setDriverClassName(driverClassName);
        //configuration
        if(initialSize != null) {
            datasource.setInitialSize(initialSize);
        }
        if(minIdle != null) {
            datasource.setMinIdle(minIdle);
        }
        if(maxActive != null) {
            datasource.setMaxActive(maxActive);
        }
        if(maxWait != null) {
            datasource.setMaxWait(maxWait);
        }
        if(timeBetweenEvictionRunsMillis != null) {
            datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
        }
        if(minEvictableIdleTimeMillis != null) {
            datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        }
        if(validationQuery!=null) {
            datasource.setValidationQuery(validationQuery);
        }
        if(testWhileIdle != null) {
            datasource.setTestWhileIdle(testWhileIdle);
        }
        if(testOnBorrow != null) {
            datasource.setTestOnBorrow(testOnBorrow);
        }
        if(testOnReturn != null) {
            datasource.setTestOnReturn(testOnReturn);
        }
        if(poolPreparedStatements != null) {
            datasource.setPoolPreparedStatements(poolPreparedStatements);
        }
        if(maxPoolPreparedStatementPerConnectionSize != null) {
            datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
        }

        if(connectionProperties != null) {
            datasource.setConnectionProperties(connectionProperties);
        }

        List<Filter> filters = new ArrayList<>();
        filters.add(statFilter());
        filters.add(wallFilter());
        datasource.setProxyFilters(filters);

        return datasource;
    }

    @Bean
    public ServletRegistrationBean druidServlet() {
        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");

        //The console administers users, adding the following two lines to the druid background requires login
        //servletRegistrationBean.addInitParameter("loginUsername", "admin");
        //servletRegistrationBean.addInitParameter("loginPassword", "admin");
        return servletRegistrationBean;
    }

    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new WebStatFilter());
        filterRegistrationBean.addUrlPatterns("/*");
        filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
        filterRegistrationBean.addInitParameter("profileEnable", "true");
        return filterRegistrationBean;
    }

    @Bean
    public StatFilter statFilter(){
        StatFilter statFilter = new StatFilter();
        statFilter.setLogSlowSql(true); //Slow SqlMillis is used to configure the standard of slow SQL, and when the execution time exceeds slow SqlMillis, it is slow.
        statFilter.setMergeSql(true); //SQL Merge Configuration
        statFilter.setSlowSqlMillis(1000);//The default value of slow SqlMillis is 3000, or 3 seconds.
        return statFilter;
    }

    @Bean
    public WallFilter wallFilter(){
        WallFilter wallFilter = new WallFilter();
        //Allow multiple SQL execution
        WallConfig config = new WallConfig();
        config.setMultiStatementAllow(true);
        wallFilter.setConfig(config);
        return wallFilter;
    }
}

mybatis-related dependencies

<!--mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.1</version>
</dependency>
<!--currency mapper-->
<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper-spring-boot-starter</artifactId>
    <version>1.1.5</version>
</dependency>
<!--pagehelper jPaginate-->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.3</version>
</dependency>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>

        <plugin>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-maven-plugin</artifactId>
            <version>1.3.5</version>
            <dependencies>
                <!--Configuration of this dependency is primarily to wait for configuration mybatis-generator.xml When you do not need to configure classPathEntry Such an attribute prevents code from being too coupled-->
                <dependency>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                    <version>5.1.44</version>
                </dependency>
                <dependency>
                    <groupId>tk.mybatis</groupId>
                    <artifactId>mapper</artifactId>
                    <version>3.4.0</version>
                </dependency>
            </dependencies>
            <executions>
                <execution>
                    <id>Generate MyBatis Artifacts</id>
                    <phase>package</phase>
                    <goals>
                        <goal>generate</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <!--Allow moving generated files -->
                <verbose>true</verbose>
                <!-- Whether to Cover -->
                <overwrite>true</overwrite>
                <!-- Auto-generated configuration -->
                <configurationFile>src/main/resources/mybatis-generator.xml</configurationFile>
            </configuration>
        </plugin>
    </plugins>
</build>

Some mybatis-related dependencies and generator configuration are introduced above, where the generator configuration file points to
The src/main/resources/mybatis-generator.xml file will be posted later.

The corresponding application.properties configuration:

#Specify the package where the bean is located
mybatis.type-aliases-package=com.dudu.domain
#Specify mapping file
mybatis.mapperLocations=classpath:mapper/*.xml

#mapper
#Comma-separated mappers with multiple interfaces
mapper.mappers=com.dudu.util.MyMapper
mapper.not-empty=false
mapper.identity=MYSQL

#pagehelper
pagehelper.helperDialect=mysql
pagehelper.reasonable=true
pagehelper.supportMethodsArguments=true
pagehelper.params=count=countSql

General Mapper Configuration

General Mapper can greatly facilitate developers, encapsulating many common methods for single form, eliminating the sql of self-writing, adding, deleting and checking.
General Mapper Plug-in Website: https://github.com/abel533/Mapper

package com.dudu.util;

import tk.mybatis.mapper.common.Mapper;
import tk.mybatis.mapper.common.MySqlMapper;

/**
 * Inherit your MyMapper
 *
 * @author
 * @since 2017-06-26 21:53
 */
public interface MyMapper<T> extends Mapper<T>, MySqlMapper<T> {
    //FIXME pays special attention to the fact that the interface can't be scanned, otherwise errors will occur.
}

The key point is that the interface can not be scanned, and can not be put together with the dao file which stores mapper.

Finally, the scanning mapper path is specified by MapperScan annotation in the startup class:

package com.dudu;
@SpringBootApplication
//Enabling Annotation Transaction Management
@EnableTransactionManagement  // Enabling annotation transaction management is equivalent to xml configuration <tx: annotation-driven/>.
@MapperScan(basePackages = "com.dudu.dao", markerInterface = MyMapper.class)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

MyBatis Generator configuration

Here, configure the mybatis-generator.xml file mentioned above, which is used to automatically generate the Model,Mapper and XML corresponding to the table, under src/main/resources.
Mybatis Geneator details: http://blog.csdn.net/isea533/article/details/42102297

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
    <!--Load the configuration file to prepare for reading the database information below-->
    <properties resource="application.properties"/>

    <context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">

        <plugin type="tk.mybatis.mapper.generator.MapperPlugin">
            <property name="mappers" value="com.dudu.util.MyMapper" />
            <!--caseSensitive default false,When database table names are case sensitive, you can set this property to true-->
          <property name="caseSensitive" v
          alue="true"/>
        </plugin>

        <!-- Prevent the generation of automatic annotations -->
        <commentGenerator>
            <property name="javaFileEncoding" value="UTF-8"/>
            <property name="suppressDate" value="true"/>
            <property name="suppressAllComments" value="true"/>
        </commentGenerator>

        <!--Database Link Address Account Password-->
        <jdbcConnection driverClass="${spring.datasource.driver-class-name}"
                        connectionURL="${spring.datasource.url}"
                        userId="${spring.datasource.username}"
                        password="${spring.datasource.password}">
        </jdbcConnection>

        <javaTypeResolver>
            <property name="forceBigDecimals" value="false"/>
        </javaTypeResolver>

        <!--generate Model Class Storage Location-->
        <javaModelGenerator targetPackage="com.dudu.domain" targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>

        <!--Generate mapping file storage location-->
        <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>

        <!--generate Dao Class Storage Location-->
        <!-- Client code to generate easy-to-use targeted Model Objects and XML Configuration file code
                type="ANNOTATEDMAPPER",generate Java Model And annotation-based Mapper object
                type="XMLMAPPER",generate SQLMap XML Documentation and independence Mapper Interface
        -->
       <javaClientGenerator type="XMLMAPPER" targetPackage="com.dudu.dao" targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
       </javaClientGenerator>

        <!--Generate corresponding tables and class names
        Remove Mybatis Generator Generated heap example
        -->
        <table tableName="LEARN_RESOURCE" domainObjectName="LearnResource" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false">
            <generatedKey column="id" sqlStatement="Mysql" identity="true"/>
        </table>
    </context>
</generatorConfiguration>

Among them, we introduced the configuration file through <properties resource="application. properties"/> so that we do not have to write to death when specifying the data source below.

Among them, tk.mybatis.mapper.generator.MapperPlugin is very important to specify the file corresponding to the generic Mapper, so that all the mappers we generate will inherit the generic Mapper.

<plugin type="tk.mybatis.mapper.generator.MapperPlugin">
    <property name="mappers" value="com.dudu.util.MyMapper" />
  <! - Case Sensitive defaults to false, which can be set to true - > when the database table name is case-sensitive.
  <property name="caseSensitive" value="true"/>
</plugin>

In this way, the corresponding files can be generated through the mybatis-generator plug-in.

If it is not IDEA development environment, you can also directly use the command: mvn mybatis-generator:generate:

The automatically generated files are shown in the following figure

Script initialization

CREATE DATABASE /*!32312 IF NOT EXISTS*/`spring` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `spring`;
DROP TABLE IF EXISTS `learn_resource`;

CREATE TABLE `learn_resource` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `author` varchar(20) DEFAULT NULL COMMENT 'author',
  `title` varchar(100) DEFAULT NULL COMMENT 'describe',
  `url` varchar(100) DEFAULT NULL COMMENT 'Address Link',
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1029 DEFAULT CHARSET=utf8;

insert into `learn_resource`(`id`,`author`,`title`,`url`) values (999,'Official SpriongBoot Example','Official SpriongBoot Example','https://github.com/spring-projects/spring-boot/tree/master/spring-boot-samples');
insert into `learn_resource`(`id`,`author`,`title`,`url`) values (1000,'Longguo College','Spring Boot Course Series Learning','http://www.roncoo.com/article/detail/124661');
insert into `learn_resource`(`id`,`author`,`title`,`url`) values (1001,'toot toot MD Independent Blog','Spring Boot Dry goods series','http://tengj.top/');
insert into `learn_resource`(`id`,`author`,`title`,`url`) values (1002,'Back-end programming toot','Spring Boot Video tutorials','http://www.toutiao.com/m1559096720023553/');

Controller layer

So far, the basic configuration is over, and we begin to implement the business logic. The Controller layer code is as follows

/** Tutorial page
 * Created by tengj on 2017/12/19
 */
@Controller
@RequestMapping("/learn")
public class LearnController  extends AbstractController{
    @Autowired
    private LearnService learnService;
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @RequestMapping("")
    public String learn(Model model){
        model.addAttribute("ctx", getContextPath()+"/");
        return "learn-resource";
    }

    /**
     * Query the list of tutorials
     * @param page
     * @return
     */
    @RequestMapping(value = "/queryLeanList",method = RequestMethod.POST)
    @ResponseBody
    public AjaxObject queryLearnList(Page<LeanQueryLeanListReq> page){
        List<LearnResource> learnList=learnService.queryLearnResouceList(page);
        PageInfo<LearnResource> pageInfo =new PageInfo<LearnResource>(learnList);
        return AjaxObject.ok().put("page", pageInfo);
    }
    /**
     * New Course
     * @param learn
     */
    @RequestMapping(value = "/add",method = RequestMethod.POST)
    @ResponseBody
    public AjaxObject addLearn(@RequestBody LearnResource learn){
        learnService.save(learn);
        return AjaxObject.ok();
    }

    /**
     * Revision of the Course
     * @param learn
     */
    @RequestMapping(value = "/update",method = RequestMethod.POST)
    @ResponseBody
    public AjaxObject updateLearn(@RequestBody LearnResource learn){
        learnService.updateNotNull(learn);
        return AjaxObject.ok();
    }

    /**
     * Delete the tutorial
     * @param ids
     */
    @RequestMapping(value="/delete",method = RequestMethod.POST)
    @ResponseBody
    public AjaxObject deleteLearn(@RequestBody Long[] ids){
        learnService.deleteBatch(ids);
        return AjaxObject.ok();
    }
}

General Service

Normally, the specific business is defined in the service of each module, and then implemented in mapper.

But bloggers look at the plug-in documentation and find the best use of a generic Mapper in Spring 4. That's the generic Service.
See here for details: https://gitee.com/free/Mapper2/blob/master/wiki/mapper/4.Spring4.md

Define Universal service Interface

/**
 * Universal Interface
 */
@Service
public interface IService<T> {

    T selectByKey(Object key);

    int save(T entity);

    int delete(Object key);

    int updateAll(T entity);

    int updateNotNull(T entity);

    List<T> selectByExample(Object example);

    //TODO Others...
}

Implementing Universal Interface Class

/**
 * General Service
 * @param <T>
 */
public abstract class BaseService<T> implements IService<T> {

    @Autowired
    protected Mapper<T> mapper;
    public Mapper<T> getMapper() {
        return mapper;
    }

    @Override
    public T selectByKey(Object key) {
        //Description: According to the primary key field for query, method parameters must contain the complete primary key properties, query conditions using equal sign
        return mapper.selectByPrimaryKey(key);
    }

    @Override
    public int save(T entity) {
        //Note: Save an entity, null attributes will also be saved, will not use the database default value
        return mapper.insert(entity);
    }

    @Override
    public int delete(Object key) {
        //Description: According to the primary key field to delete, method parameters must contain the complete primary key properties.
        return mapper.deleteByPrimaryKey(key);
    }

    @Override
    public int updateAll(T entity) {
        //Note: Update all fields of entity according to primary key, null value will be updated
        return mapper.updateByPrimaryKey(entity);
    }

    @Override
    public int updateNotNull(T entity) {
        //Update values of attributes that are not null based on primary keys
        return mapper.updateByPrimaryKeySelective(entity);
    }

    @Override
    public List<T> selectByExample(Object example) {
        //Description: Query according to Example condition
        //Important: This query supports specifying query columns by Example class and query columns by selectProperties method
        return mapper.selectByExample(example);
    }
}

At this point, the basic addition, deletion and modification of the general service will be written, and the service of the specific business can directly inherit this interface, and additional methods can be added, such as:

public interface LearnService  extends IService<LearnResource>{
    public List<LearnResource> queryLearnResouceList(Page<LeanQueryLeanListReq> page);
    public void deleteBatch(Long[] ids);
}

Specific implementation of service

/**
 * Created by tengj on 2017/4/7.
 */
@Service
public class LearnServiceImpl extends BaseService<LearnResource>  implements LearnService {

    @Autowired
    private LearnResourceMapper  learnResourceMapper;

    @Override
    public void deleteBatch(Long[] ids) {
        Arrays.stream(ids).forEach(id->learnResourceMapper.deleteByPrimaryKey(id));
    }

    @Override
    public List<LearnResource> queryLearnResouceList(Page<LeanQueryLeanListReq> page) {
        PageHelper.startPage(page.getPage(), page.getRows());
        return learnResourceMapper.queryLearnResouceList(page.getCondition());
    }
}

As you can see, two methods have been implemented on the specific LearnService Impl side. The others all use the generic service, leaving a lot of effort in development.

Mapper correlation

Implementing sevice customization in automatically generated mapper files:

public interface LearnResourceMapper extends MyMapper<LearnResource> {
    List<LearnResource> queryLearnResouceList(Map<String,Object> map);
}

LearnResourceMapper.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dudu.dao.LearnResourceMapper">
  <resultMap id="BaseResultMap" type="com.dudu.domain.LearnResource">
    <!--
      WARNING - @mbg.generated
    -->
    <id column="id" jdbcType="BIGINT" property="id" />
    <result column="author" jdbcType="VARCHAR" property="author" />
    <result column="title" jdbcType="VARCHAR" property="title" />
    <result column="url" jdbcType="VARCHAR" property="url" />
  </resultMap>
    <select id="queryLearnResouceList" resultType="com.dudu.domain.LearnResource">
      SELECT * from learn_resource where 1=1
      <if test="author != null and author!= ''">
        and author like CONCAT('%',#{author},'%')
      </if>
      <if test="title != null and title!= ''">
        and title like CONCAT('%',#{title},'%')
      </if>
      order by id desc
    </select>
</mapper>

IDEA can install this plug-in so that it can jump directly from Mapper file to xml

The final effect of the project is as follows, adding, deleting, modifying and checking pages are not a few:

As mentioned above, druid has a corresponding monitoring interface, which is input after starting the project. http://localhost:8090/spring/druid You can log in, the interface effect is as follows

summary

At this point, a set of Spring Boot application templates suitable for enterprise development is good, Mybatis + General Mapper, Mybatis Geneator can really save a lot of development costs and improve efficiency. Front-end integration of vue.js, see the source code.

For more Spring Boot dry goods tutorials, go to: Outline of Spring Boot Dry Goods Series

Source Download

 ( ̄︶ ̄)↗[Relevant sample complete code]
- chapter11=="Spring Boot Dry Goods Series: (11) Data Storage Chapter - Spring Boot Integrated Mybatis Universal Mapper Plug-in

If you want the ace template source code, reply to the keyword in the blogger's public number:

I always feel that I am not writing technology, but feelings, an article is a trace of my own way. Professional skills are the most reproducible way to succeed. I hope that my road can help you avoid detours. I hope that I can help you erase the dust of knowledge. I hope I can help you clear up the vein of knowledge. I hope you have me on the top of technology in the future.

Posted by kender on Tue, 08 Jan 2019 23:36:10 -0800