spring-boot指南

/ Spring Boot / 没有评论 / 110浏览

spring boot入门指南,基于1.5.10.RELEAS

介绍

spring-boot-web

web快速入门

介绍

web入门

一个简单的demo,以及spring-boot的约定(套路)

依赖

<!-- web依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>${spring.boot.version}</version>
</dependency>

插件

<plugins>
    <!-- java 1.8编译,推荐 -->
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.0</version>
        <configuration>
            <source>1.8</source>
            <target>1.8</target>
        </configuration>
    </plugin>
    
    <!-- spring boot插件 -->
    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>${spring.boot.version}</version>
        <executions>
            <execution>
                <goals>
                    <goal>repackage</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
</plugins>

项目结构

推荐的如下结构

com
 +- example
     +- myproject
         +- Application.java
         |
         +- domain
         |   +- Customer.java
         |   +- CustomerRepository.java
         |
         +- service
         |   +- CustomerService.java
         |
         +- web
             +- CustomerController.java

Application为启动类,推荐在包的外层,这样可以保证到子包中的bean可以被默认扫描到

入口类

入口类采用注解@SpringBootApplication,会自动配置扫包等

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

自定义配置

自定义配置一般推荐javaconfig的形式配置,添加@Configuration

@Configuration
public class WebConfiguration {

    /**
     * 配置RestTemplate,默认实现为JDK的URLConnection
     *
     * @return RestTemplate
     */
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

}

这样就配置了一个bean,与xml中<bean class="org.springframework.web.client.RestTemplat"/>等同

运行

日志打印

全局配置

application.yml中配置即可

# 设置日志级别,当然我们也可以采用配置文件配置
logging:
  level:
    root: info
    # com.example.springboot包日志打印级别
    com:
      example:
        springboot: debug
  # 自定义logback日志配置,推荐命名为logback-spring.xml
  # config: logback-spring.xml

自定义logback配置文件

首先在application.yml中配置日志路径

logging:
  # 自定义logback日志配置,推荐命名为logback-spring.xml
  config: logback-spring.xml

然后配置logback日志即可,如果命名为logback-spring.xml则会自动配置,不需要指定

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
    <!--定义日志文件的存储地址 勿在logback的配置中使用相对路径-->
    <property name="LOG_HOME" value="/home"/>

    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH🇲🇲ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 按照每天生成日志文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <FileNamePattern>${LOG_HOME}/xxx.log.%d{yyyy-MM-dd}.log</FileNamePattern>
            <!--日志文件保留天数-->
            <MaxHistory>15</MaxHistory>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH🇲🇲ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
        <!--日志文件最大的大小-->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>50MB</MaxFileSize>
        </triggeringPolicy>
    </appender>

    <!--日志异步到数据库 -->
    <appender name="DB" class="ch.qos.logback.classic.db.DBAppender">
        <!--日志异步到数据库 -->
        <connectionSource class="ch.qos.logback.core.db.DriverManagerConnectionSource">
            <!--连接池 -->
            <dataSource class="com.mchange.v2.c3p0.ComboPooledDataSource">
                <driverClass>com.mysql.jdbc.Driver</driverClass>
                <url>jdbc:mysql://127.0.0.1:3306/databaseName</url>
                <user>root</user>
                <password>root</password>
            </dataSource>
        </connectionSource>
    </appender>

    <!-- show parameters for hibernate sql 专为Hibernate定制 -->
    <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/>
    <logger name="org.hibernate.type.descriptor.sql.BasicExtractor" level="DEBUG"/>
    <logger name="org.hibernate.SQL" level="DEBUG"/>
    <logger name="org.hibernate.engine.QueryParameters" level="DEBUG"/>
    <logger name="org.hibernate.engine.query.HQLQueryPlan" level="DEBUG"/>

    <!-- myibatis configure -->
    <logger name="com.apache.ibatis" level="TRACE"/>
    <logger name="java.sql.Connection" level="DEBUG"/>
    <logger name="java.sql.Statement" level="DEBUG"/>
    <logger name="java.sql.PreparedStatement" level="DEBUG"/>

    <!-- 日志输出级别 -->
    <root level="INFO">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="FILE"/>
    </root>
</configuration>

打日志

我们采用slf4j接口,不要用具体的实现,面向接口

package com.example.springboot.web.controller;

import com.example.springboot.web.model.Demo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/demo")
public class DemoController {

    private static final Logger LOG = LoggerFactory.getLogger(DemoController.class);

    @GetMapping("/{id}")
    public Demo show(@PathVariable String id) {
        LOG.debug("show: {}", id);
        return new Demo(id, id);
    }

}

常用日志级别debuginfowarnerror

提示,一定要用占位符{}输入内容。

public class LogController {

    private static final Logger LOG = LoggerFactory.getLogger(DemoController.class);

    public void doLog() {
        // 正确姿势
        LOG.debug("show: {}", id);
        
        // 枪毙
        LOG.debug("show" + id);
    }

}

Debug模式

spring-boot由于自动配置,很多时候我们都不知道自动配置了哪些bean,这时可以打开debug模式

debug: true

测试

依赖

pom添加依赖spring-boot-starter-test

<!-- 测试 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>${spring.boot.version}</version>
    <scope>test</scope>
</dependency>

测试类

测试类均放在test/java目录下,加上测试相关的注解

package com.example.springboot.web;

import com.example.springboot.web.model.Demo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate;

@SpringBootTest(classes = Application.class)
@RunWith(SpringRunner.class)
public class ApplicationTest {

    private static final Logger LOG = LoggerFactory.getLogger(ApplicationTest.class);

    private static final String SERVICE_SHOW = "http://127.0.0.1:8080/demo/1";

    @Autowired
    private RestTemplate restTemplate;

    @Test
    public void show() {
        ResponseEntity<Demo> entity = restTemplate.getForEntity(SERVICE_SHOW, Demo.class);
        if (entity.getStatusCode() == HttpStatus.OK) {
            LOG.info("结果: {}", entity.getBody());
        }
    }

}

补充

启动运行代码

很多时候我们希望在启动时运行一些代码,比如加载一些东东,那么你需要使用CommandLineRunner

import org.springframework.boot.*;
import org.springframework.stereotype.*;

@Component
public class MyBean implements CommandLineRunner {

    public void run(String... args) {
        // Do something...
    }

}

你可以实现org.springframework.core.Ordered接口或添加org.springframework.core.annotation.Order注解按顺序执行

yaml使用

你也许想把一些配置写在配置文件中,代码中使用这些配置,例如

foo:
  list:
    - name: my name
      description: my description
    - name: my name2
      description: my description2

将其映射为Java对象,MyPojo需要拥有namedescription属性

@Component
@ConfigurationProperties("foo")
public class FooProperties {

    private final List<MyPojo> list = new ArrayList<>();

    public List<MyPojo> getList() {
        return this.list;
    }

}

加上@Component后,FooProperties就可以直接在代码中注入了

Profile

在生产中,我们肯定有很多环境,devtestprod

我们的配置文件可以以application-profile.yml的形式命名,然后指定使用的profile

spring-boot-mybatis

快速集成mybatis

介绍

数据源

要操作数据库肯定离不开数据源,下面介绍数据源的配置,这里以Hikari数据源为例

依赖

这里使用mysql数据,依赖即可

<dependencies>
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>2.6.2</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.40</version>
    </dependency>
</dependencies>

配置数据源

我们配置数据库连接信息

spring:
  datasource:
    # hikari数据源配置
    hikari:
      driver-class-name: com.mysql.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3306/demo?userUnicode=true&characterEncoding=UTF8&useSSL=false
      username: root
      password: 123456

这里简单的配置了下,并未设置一些例如最大连接数、超时等参数,详细参数见Hikari介绍。

配置一个DataSource bean

/**
 * 配置Hikari数据源
 */
@Configuration
public class DataSourceConfiguration {

    private static final String PREFIX = "spring.datasource.hikari";

    @Bean
    @ConfigurationProperties(prefix = PREFIX)
    public HikariConfig hikariConfig() {
        return new HikariConfig();
    }

    @Bean
    @Primary
    public DataSource dataSource(HikariConfig hikariConfig) {
        return new HikariDataSource(hikariConfig);
    }

}

注解ConfigurationProperties会将配置属性映射成POJO,更详细的使用见spring-boot文档。

测试数据源

编写单元测试测试下数据源是否配置正确

@SpringBootTest(classes = Application.class)
@RunWith(SpringRunner.class)
public class ApplicationTest {

    private static final Logger LOG = LoggerFactory.getLogger(ApplicationTest.class);

    @Autowired
    private DataSource dataSource;

    @Test
    public void dataSource() {
        LOG.info("dataSource: {}", dataSource.getClass());
    }

}

集成mybatis

这里介绍mybatis官网注解方式集成,xml就算了。。

依赖

依赖mybatis官方的mybatis-spring-boot-starter,并排除掉tomcat-jdbc数据源,用Hikari数据源

<dependencies>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.2</version>
        <exclusions>
            <exclusion>
                <groupId>org.apache.tomcat</groupId>
                <artifactId>tomcat-jdbc</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

配置mybatis

我们配置mapper的xml文件位置以及包别名

mybatis:
  # mapper的xml位置
  mapper-locations: classpath:mapper/*Mapper.xml
  # 包别名
  type-aliases-package: com.example.springboot.mybatis.entity

启动类添加MapperScan注解扫描mapper接口,当然也可以不添加,因为对应的Mapper接口上添加了@Mapper注解

@SpringBootApplication
// 也可以不用加扫描mapper接口
@MapperScan(AppConstant.MAPPER_SCAN_PACKAGE)
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

这样最简单的环境就配置成功了

编写demo

实体类

我们搞个实体类,其中Entity有一个id字段

public class Demo extends Entity {

    @NotBlank
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Demo{" +
                "name='" + name + '\'' +
                "} " + super.toString();
    }
}
Mapper接口

面向数据库的一些操作

@Mapper
public interface DemoDao {

    int insert(Demo demo);

    int updateById(Demo demo);

    int deleteById(@Param("id") Long id);

    Demo findById(@Param("id") Long id);

    List<Demo> findAll();

}

Mapper注解是mybatis提供的,标记为一个Mapper,会被spring容器管理

Service

下面简单的写下接口及其实现

public interface DemoService {

    int insert(Demo demo);

    int updateById(Demo demo);

    int deleteById(Long id);

    Demo findById(Long id);

    List<Demo> findAll();

}

@Service
@Transactional
public class DemoServiceImpl implements DemoService {

    @Autowired
    private DemoDao demoDao;

    @Override
    public int insert(Demo demo) {
        return demoDao.insert(demo);
    }

    @Override
    public int updateById(Demo demo) {
        return demoDao.updateById(demo);
    }

    @Override
    public int deleteById(Long id) {
        return demoDao.deleteById(id);
    }

    @Override
    public Demo findById(Long id) {
        return demoDao.findById(id);
    }

    @Override
    public List<Demo> findAll() {
        return demoDao.findAll();
    }

}

Transactional注解开启事务

Controller

简单rest接口

@RestController
@RequestMapping(DemoController.PATH)
@Validated
public class DemoController {

    static final String PATH = "/demo";

    @Autowired
    private DemoService demoService;

    @PostMapping
    public Rest<Demo> add(@Validated @RequestBody Demo demo) {
        demoService.insert(demo);
        return Rest.ok(MessageConstant.OK, demo);
    }

    @PutMapping
    public Rest<Demo> modify(@Validated @RequestBody Demo demo) {
        demoService.updateById(demo);
        return Rest.ok(MessageConstant.OK, demo);
    }

    @DeleteMapping("/{id}")
    public Rest<Integer> remove(@PathVariable Long id) {
        return Rest.ok(MessageConstant.OK, demoService.deleteById(id));
    }

    @GetMapping("/{id}")
    public Rest<Demo> find(@PathVariable Long id) {
        return Rest.ok(MessageConstant.OK, demoService.findById(id));
    }

    @GetMapping
    public Rest<List<Demo>> findAll() {
        return Rest.ok(MessageConstant.OK, demoService.findAll());
    }

}

其中Validated注解与校验参数有关,以后会与统一异常处理一起专门讲解

运行demo

运行后可用postman进行测试接口,略

补充

上面简单的集成了mybatis,但实际开发中还不够,我们需要通用Mapper、分页

这里介绍java config手动集成方式,对于官方提供的starter,这里不做集成与介绍。

集成pagehelper

我们在配置好了mybatis-spring-boot-starter的基础上再集成

依赖

依赖pagehelper即可

<!-- mybatis分页插件 -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.1.2</version>
</dependency>
配置bean

PageInterceptor注册为bean,即可被自动扫描,添加到mybatis的plugin中取

@Configuration
public class MybatisConfiguration {

    private static final String PAGE_HELPER_PREFIX = "pageHelper";

    /**
     * 分页插件配置
     *
     * @return Properties
     */
    @Bean
    @ConfigurationProperties(PAGE_HELPER_PREFIX)
    public Properties pageProperties() {
        return new Properties();
    }

    /**
     * 注册分页插件
     *
     * @return PageInterceptor
     */
    @Bean
    public Interceptor pageInterceptor(Properties pageProperties){
        Interceptor pageInterceptor = new PageInterceptor();
        pageInterceptor.setProperties(pageProperties);
        return pageInterceptor;
    }

}
插件配置

我们还是习惯将将插件参数配置在application.yml,方便进行统一管理

# 分页
pageHelper:
  helperDialect: mysql
  reasonable: "true"
  params: count=countSql
测试

编写单元测试

@SpringBootTest(classes = Application.class)
@RunWith(SpringRunner.class)
public class DemoDaoTest {

    private static final Logger LOG = LoggerFactory.getLogger(ApplicationTest.class);

    @Autowired
    private DemoDao demoDao;

    @Test
    public void page() {
        PageHelper.startPage(2, 2);
        List<Demo> demos = demoDao.findAll();
        LOG.info("demos: {}", demos);
    }

}

集成通用Mapper

我们还是在配置好了mybatis-spring-boot-starter的基础上再集成

为了利用mybatis官方优秀的自动配置,将通用Mapper的配置加入到其中

当然,如果用mapper-spring-boot-starter则很简单了,直接看通用mapper的官网即可,这里不做介绍

依赖

推荐用4.0的

<!-- mybatis通用mapper -->
<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper</artifactId>
    <version>4.0.0</version>
</dependency>
修改mybatis官方的自动配置

我们建一个org.mybatis.spring.boot.autoconfigure包,重写MybatisAutoConfiguration

主要添加了wrapperConf方法,替换为通用Mapper的Configuration

再将通用Mapper需要的配置定义为属性,让外部注入

/**
 *    Copyright 2015-2017 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.mybatis.spring.boot.autoconfigure;

import java.util.List;
import java.util.Properties;

import javax.annotation.PostConstruct;
import javax.sql.DataSource;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.mapper.ClassPathMapperScanner;
import org.mybatis.spring.mapper.MapperFactoryBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import tk.mybatis.mapper.mapperhelper.MapperHelper;

/**
 * {@link EnableAutoConfiguration Auto-Configuration} for Mybatis. Contributes a
 * {@link SqlSessionFactory} and a {@link SqlSessionTemplate}.
 *
 * If {@link org.mybatis.spring.annotation.MapperScan} is used, or a
 * configuration file is specified as a property, those will be considered,
 * otherwise this auto-configuration will attempt to register mappers based on
 * the interface definitions in or under the root auto-configuration package.
 *
 * @author Eddú Meléndez
 * @author Josh Long
 * @author Kazuki Shimizu
 * @author Eduardo Macarrón
 */
@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnBean(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisAutoConfiguration {

    private static final Logger logger = LoggerFactory.getLogger(MybatisAutoConfiguration.class);

    private final MybatisProperties properties;

    private final Properties mapperProperties;

    private final Interceptor[] interceptors;

    private final ResourceLoader resourceLoader;

    private final DatabaseIdProvider databaseIdProvider;

    private final List<ConfigurationCustomizer> configurationCustomizers;

    public MybatisAutoConfiguration(MybatisProperties properties,
                                    Properties mapperProperties,
                                    ObjectProvider<Interceptor[]> interceptorsProvider,
                                    ResourceLoader resourceLoader,
                                    ObjectProvider<DatabaseIdProvider> databaseIdProvider,
                                    ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
        this.properties = properties;
        this.mapperProperties = mapperProperties;
        this.interceptors = interceptorsProvider.getIfAvailable();
        this.resourceLoader = resourceLoader;
        this.databaseIdProvider = databaseIdProvider.getIfAvailable();
        this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
    }

    @PostConstruct
    public void checkConfigFileExists() {
        if (this.properties.isCheckConfigLocation() && StringUtils.hasText(this.properties.getConfigLocation())) {
            Resource resource = this.resourceLoader.getResource(this.properties.getConfigLocation());
            Assert.state(resource.exists(), "Cannot find config location: " + resource
                    + " (please add config file or check your Mybatis configuration)");
        }
    }

    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        factory.setVfs(SpringBootVFS.class);
        if (StringUtils.hasText(this.properties.getConfigLocation())) {
            factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
        }
        Configuration configuration = this.properties.getConfiguration();
        if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) {
            configuration = new Configuration();
        }
        if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) {
            for (ConfigurationCustomizer customizer : this.configurationCustomizers) {
                customizer.customize(configuration);
            }
        }

        // 这里替换为mapper中的Configuration,这样可以利用mybatis官方优秀的配置
        wrapperConf(factory, configuration);

        if (this.properties.getConfigurationProperties() != null) {
            factory.setConfigurationProperties(this.properties.getConfigurationProperties());
        }
        if (!ObjectUtils.isEmpty(this.interceptors)) {
            factory.setPlugins(this.interceptors);
        }
        if (this.databaseIdProvider != null) {
            factory.setDatabaseIdProvider(this.databaseIdProvider);
        }
        if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
            factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
        }
        if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
            factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
        }
        if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
            factory.setMapperLocations(this.properties.resolveMapperLocations());
        }

        return factory.getObject();
    }

    /**
     * 将mybatis官方的Configuration替换为通用Mapper的configuration
     *
     * @param factory SqlSessionFactoryBean
     * @param configuration mybatis官方的Configuration
     */
    private void wrapperConf(SqlSessionFactoryBean factory, Configuration configuration) {
        tk.mybatis.mapper.session.Configuration conf = new tk.mybatis.mapper.session.Configuration();
        if (configuration != null) {
            BeanUtils.copyProperties(configuration, conf);
        }
        conf.setMapperHelper(new MapperHelper(this.mapperProperties));
        factory.setConfiguration(conf);
    }

    @Bean
    @ConditionalOnMissingBean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        ExecutorType executorType = this.properties.getExecutorType();
        if (executorType != null) {
            return new SqlSessionTemplate(sqlSessionFactory, executorType);
        } else {
            return new SqlSessionTemplate(sqlSessionFactory);
        }
    }

    /**
     * This will just scan the same base package as Spring Boot does. If you want
     * more power, you can explicitly use
     * {@link org.mybatis.spring.annotation.MapperScan} but this will get typed
     * mappers working correctly, out-of-the-box, similar to using Spring Data JPA
     * repositories.
     */
    public static class AutoConfiguredMapperScannerRegistrar
            implements BeanFactoryAware, ImportBeanDefinitionRegistrar, ResourceLoaderAware {

        private BeanFactory beanFactory;

        private ResourceLoader resourceLoader;

        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

            logger.debug("Searching for mappers annotated with @Mapper");

            ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);

            try {
                if (this.resourceLoader != null) {
                    scanner.setResourceLoader(this.resourceLoader);
                }

                List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
                if (logger.isDebugEnabled()) {
                    for (String pkg : packages) {
                        logger.debug("Using auto-configuration base package '{}'", pkg);
                    }
                }

                scanner.setAnnotationClass(Mapper.class);
                scanner.registerFilters();
                scanner.doScan(StringUtils.toStringArray(packages));
            } catch (IllegalStateException ex) {
                logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.", ex);
            }
        }

        @Override
        public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
            this.beanFactory = beanFactory;
        }

        @Override
        public void setResourceLoader(ResourceLoader resourceLoader) {
            this.resourceLoader = resourceLoader;
        }
    }

    /**
     * {@link org.mybatis.spring.annotation.MapperScan} ultimately ends up
     * creating instances of {@link MapperFactoryBean}. If
     * {@link org.mybatis.spring.annotation.MapperScan} is used then this
     * auto-configuration is not needed. If it is _not_ used, however, then this
     * will bring in a bean registrar and automatically register components based
     * on the same component-scanning path as Spring Boot itself.
     */
    @org.springframework.context.annotation.Configuration
    @Import({ AutoConfiguredMapperScannerRegistrar.class })
    @ConditionalOnMissingBean(MapperFactoryBean.class)
    public static class MapperScannerRegistrarNotFoundConfiguration {

        @PostConstruct
        public void afterPropertiesSet() {
            logger.debug("No {} found.", MapperFactoryBean.class.getName());
        }
    }

}
配置通用Mapper的配置bean

因为修改后的自动配置依赖外部提供通用mapper的配置,我们定义即可

@Configuration
public class MybatisConfiguration {

    private static final String MAPPER_HELPER_PREFIX = "mapper";

    /**
     * 通用mapper的配置
     *
     * @return Properties
     */
    @Bean
    @ConfigurationProperties(MAPPER_HELPER_PREFIX)
    public Properties mapperProperties() {
        return new Properties();
    }

}
自定义Mapper接口

不直接集成Mapper,而是自定义一个

package com.example.springboot.mybatis.util;

import tk.mybatis.mapper.common.Mapper;

/**
 * 自定义Mapper接口
 *
 * @param <T> 实体
 */
public interface MyMapper<T> extends Mapper<T> {
}

全局配置

我们还是习惯将将通用mapper参数配置在application.yml,方便进行统一管理

mapper:
  mappers:
    - com.example.springboot.mybatis.util.MyMapper
  not-empty: true
修改entity

我们添加IdTable等注解

Mapper接口继承自定义Mapper接口

继承我们自定义的Mapper

@Mapper
public interface DemoDao extends MyMapper<Demo> {
}
测试

编写单元测试

@SpringBootTest(classes = Application.class)
@RunWith(SpringRunner.class)
public class DemoDaoTest {

    private static final Logger LOG = LoggerFactory.getLogger(ApplicationTest.class);

    @Autowired
    private DemoDao demoDao;

    @Test
    public void selectAll() {
        List<Demo> demos = demoDao.selectAll();
        LOG.info("demos: {}", demos);
    }

    @Test
    public void select() {
        Demo demo = new Demo();
        demo.setName("张三");
        List<Demo> demos = demoDao.select(demo);
        LOG.info("demos: {}", demos);
    }
}

spring-boot-redis

快速集成redis

介绍

集成redis

依赖

依赖spring-boot-starter-data-redis模块

<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>${spring.boot.version}</version>
</dependency>

全局配置

application.yml中配置redis的连接信息

spring:
  # redis配置
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    pool:
      max-active: 16
      max-idle: 8

支持哨兵、cluster模式,参考官网配置

RedisAutoConfiguation中可以看到springboot默认帮我们配置了RedisTemplateStringRedisTemplate两个操作redis的模板bean

使用

简单的使用StringRedisTemplate进行string数据结构的操作

@SpringBootTest(classes = Application.class)
@RunWith(SpringRunner.class)
public class ApplicationTest {

    private static final Logger LOG = LoggerFactory.getLogger(ApplicationTest.class);

    private static final String KEY = "com.example.spring.boot";
    private static final String VALUE = "redis";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void setAndGet() {
        BoundValueOperations<String, String> ops = stringRedisTemplate.boundValueOps(KEY);
        ops.set(VALUE);
        LOG.info("get key[{}] -> {}", KEY, ops.get());
    }

}

补充

spring-boot-starter-data-redis模块帮我们自动配置了Jedis,往往我们会有分布式锁、异步等需求,这时redisson是一个更好的选择。

redisson

redisson我们用的最多的就是分布式锁,下面介绍与spring-boot的集成以及分布式锁的使用

依赖
<!-- redisson -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.7.0</version>
</dependency>
配置single节点

我们测试一般用redis单实例,因此redisson配置单实例即可(支持哨兵、cluster)

@Configuration
public class RedissonConfiguration {

    /**
     * 配置单实例
     * 
     * @return Config
     */
    @Bean
    public Config config() {
        Config config = new Config();
        config.useSingleServer()
                .setAddress("localhost:6379")
                .setDatabase(0);
        return config;
    }

    @Bean
    public RedissonClient redissonClient(Config config) {
        return Redisson.create(config);
    }

}

这里就简单配置单实例,其他配置采用默认配置

分布式锁模板代码
@SpringBootTest(classes = Application.class)
@RunWith(SpringRunner.class)
public class ApplicationTest {

    private static final Logger LOG = LoggerFactory.getLogger(ApplicationTest.class);

    private static final String LOCK_KEY = "lock";

    @Autowired
    private RedissonClient redissonClient;

    @Test
    public void lock() {
        RLock lock = redissonClient.getLock(LOCK_KEY);
        try {
            if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
                try {
                    LOG.info("获取到锁:{}", LOCK_KEY);
                    // 做一些事情
                    Thread.sleep(2000L);
                } finally {
                    lock.unlock();
                    LOG.info("释放锁:{}", LOCK_KEY);
                }
            } else {
                LOG.warn("没有获取到锁:{}", LOCK_KEY);
            }
        } catch (InterruptedException e) {
            LOG.error("获取锁被打断:{}", LOCK_KEY);
        }
    }

}

spring-boot-mongo

mongodb快速入门

介绍

mongo入门

依赖

依赖spring-boot-starter-data-mongodb模块即可

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
    <version>${spring.boot.version}</version>
</dependency>

配置

全局配置文件中配置mongodb的连接信息

spring:
  data:
    mongodb:
      uri: mongodb://{username}:{password}@{ip}:{port}/{database}

修改其中的变量即可

spring-boot-starter-data-mongodb模块会自动帮我们配置MongoTemplate模板bean,通过它操作mongodb,就像JdbcTemplate

entity

创建一个实体与mongo中的bson对应,类似关系型数据库中的table

创建一个通用父类,包含常用字段

public abstract class Entity implements Serializable {

    @Id
    private String id;
    private Date createAt;
    private Date updateAt;
}

具体的实体类继承,Document注解相当于JPA中的Table注解,映射字段

@Document(collection = "T_Demo")
public class Demo extends Entity {

    private String name;

}

测试

编写单元测试,做下简单的操作

@SpringBootTest(classes = Application.class)
@RunWith(SpringRunner.class)
public class ApplicationTest {

    private static final Logger LOG = LoggerFactory.getLogger(ApplicationTest.class);

    @Autowired
    private MongoTemplate mongoTemplate;

    @Test
    public void save() {
        Demo demo = new Demo();
        demo.setName("奔波儿灞");
        mongoTemplate.save(demo);
    }

    @Test
    public void find() {
        List<Demo> demos = mongoTemplate.findAll(Demo.class);
        LOG.info("demos: {}", demos);
    }

}

通过MongoTemplate,我们可以方便的对mongodb进行操作,比mongo driver好用的多

补充

去掉_class字段

可以发现我们通过MongoTemplate操作,会多一个_class字段,下面介绍如何去掉。

配置映射

配置一个MappingMongoConverterbean,去掉_class字段

@Configuration
public class MongoConfiguration {

    /**
     * java对象与mongo bson数据的映射配置,去掉插入数据库后_class字段
     *
     * @param factory MongoDbFactory
     * @param context MongoMappingContext
     * @return MappingMongoConverter
     */
    @Bean
    public MappingMongoConverter mappingMongoConverter(MongoDbFactory factory, MongoMappingContext context) {
        DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);
        MappingMongoConverter mappingConverter = new MappingMongoConverter(dbRefResolver, context);
        // mongodb中不保存_class属性
        mappingConverter.setTypeMapper(new DefaultMongoTypeMapper(null));
        return mappingConverter;
    }

}

通用crud

对基本的增删改查say no

定义接口

抽象出通用接口,实体需要继承Entity父类

public interface BaseDao<T extends Entity> {

    /**
     * 根据id查询记录
     *
     * @param id 记录id
     * @return T
     */
    T findById(String id);

    /**
     * 批量获取记录
     *
     * @param ids 记录id集合
     * @return List<T>
     */
    List<T> findByIds(List<String> ids);

    /**
     * 查询一条记录
     *
     * @param query Query
     * @return T
     */
    T findOne(Query query);

    /**
     * 查询多条记录
     *
     * @param query Query
     * @return List<T>
     */
    List<T> find(Query query);

    /**
     * 统计记录条数
     *
     * @param query Query
     * @return Long
     */
    Long count(Query query);

    /**
     * 插入记录
     *
     * @param entity T
     */
    void insert(T entity);

    /**
     * 插入或更新记录
     *
     * @param entity T
     */
    void save(T entity);

    /**
     * 更新所有满足条件的记录
     *
     * @param query Query
     * @param update Update
     */
    void update(Query query, Update update);

    /**
     * 根据id删除记录
     *
     * @param id 记录id
     */
    void deleteById(String id);

    /**
     * 批量删除记录
     *
     * @param ids 记录集合
     */
    void deleteByIds(List<String> ids);

    /**
     * 删除记录
     *
     * @param query Query
     */
    void delete(Query query);

    /**
     * 分页获取数据,传统分页,适用于数据量不是很大的情况,内部采用skip、limit
     *
     * @param criteria 查询条件
     * @param pageable 分页对象
     * @return Page<T>
     */
    Page<T> page(Criteria criteria, Pageable pageable);
}
实现

做过Hibernate的通用BaseDao的人应该特别熟悉,主要是利用范型的基本知识

public abstract class BaseDaoImpl<T extends Entity> implements BaseDao<T> {

    private static final String ID_KEY = "_id";
    private static final String UPDATE_KEY = "updateAt";

    @Autowired
    private MongoTemplate mongoTemplate;

    private Class<T> entityClazz;

    @SuppressWarnings({"unchecked"})
    public BaseDaoImpl() {
        Type genericType = getClass().getGenericSuperclass();
        Type[] params = ((ParameterizedType) genericType).getActualTypeArguments();
        entityClazz = (Class) params[0];
    }

    @Override
    public T findById(String id) {
        return mongoTemplate.findById(id, entityClazz);
    }

    @Override
    public List<T> findByIds(List<String> ids) {
        return mongoTemplate.find(query(Criteria.where(ID_KEY).in(ids)), entityClazz);
    }

    @Override
    public T findOne(Query query) {
        return mongoTemplate.findOne(query, entityClazz);
    }

    @Override
    public List<T> find(Query query) {
        return mongoTemplate.find(query, entityClazz);
    }

    @Override
    public Long count(Query query) {
        return mongoTemplate.count(query, entityClazz);
    }

    @Override
    public void insert(T entity) {
        if (Objects.isNull(entity.getCreateAt())) {
            entity.setCreateAt(new Date());
        }
        if (Objects.isNull(entity.getUpdateAt())) {
            entity.setUpdateAt(new Date());
        }
        mongoTemplate.insert(entity);
    }

    @Override
    public void save(T entity) {
        Date now = new Date();
        if (Objects.isNull(entity.getCreateAt())) {
            entity.setCreateAt(now);
        }
        entity.setUpdateAt(now);
        mongoTemplate.save(entity);
    }

    @Override
    public void update(Query query, Update update) {
        Object updateAt = update.getUpdateObject().get(UPDATE_KEY);
        if (Objects.isNull(updateAt)) {
            update.set(UPDATE_KEY, new Date());
        }
        mongoTemplate.updateMulti(query, update, entityClazz);
    }

    @Override
    public void deleteById(String id) {
        mongoTemplate.remove(query(Criteria.where(ID_KEY).is(id)), entityClazz);
    }

    @Override
    public void deleteByIds(List<String> ids) {
        mongoTemplate.remove(query(Criteria.where(ID_KEY).in(ids)), entityClazz);
    }

    @Override
    public void delete(Query query) {
        mongoTemplate.remove(query, entityClazz);
    }

    @Override
    public Page<T> page(Criteria criteria, Pageable pageable) {
        Query query = Query.query(criteria);
        Long count = count(query);
        if (count == 0L) {
            return new PageImpl<>(Collections.emptyList(), pageable, count);
        }
        List<T> list = find(query.with(pageable));
        return new PageImpl<>(list, pageable, count);
    }

    protected Query query(Criteria criteria) {
        return Query.query(criteria);
    }

    protected MongoTemplate getMongoTemplate() {
        return mongoTemplate;
    }
}
使用

其他实体类继承Entity,编写dao接口、实现类

dao接口特别简单,继承BaseDao

public interface DemoDao extends BaseDao<Demo> {
}

实现类也特别简单,继承BaseDaoImpl

@Repository
public class DemoDaoImpl extends BaseDaoImpl<Demo> implements DemoDao {
}

这样就获得了通用的能力

测试

简单测试下

@SpringBootTest(classes = Application.class)
@RunWith(SpringRunner.class)
public class DemoDaoTest {

    private static final Logger LOG = LoggerFactory.getLogger(DemoDaoTest.class);

    @Autowired
    private DemoDao demoDao;

    @Test
    public void page() {
        // 第几页从0开始。。
        Pageable page = new PageRequest(0, 10);
        Page<Demo> demoPage = demoDao.page(new Criteria(), page);
        LOG.info("totalPage: {}, totalElements: {}, data: {}", demoPage.getTotalPages(), demoPage.getTotalElements(), demoPage.getContent());
    }

}

controller中优雅的分页

利用PageableDefault注解将分页参数反射到Pageable的实现类

@RestController
@RequestMapping("/demo")
public class DemoController {

    @Autowired
    private DemoService demoService;

    @GetMapping
    public Page<Demo> page(@PageableDefault(page = 0, size = 20) Pageable pageable) {
        return demoService.page(pageable);
    }

}

分页参数:pagesizesort

查询第2页,每页1条数据:http://127.0.0.1:8080/demo?page=1&size=1

注意:page是从0开始的

根据创建时间降序:http://127.0.0.1:8080/demo?page=0&size=10&sort=createAt,desc

提示:多个字段进行排序,sort=字段1,desc|asc&sort=字段2,desc|asc