Druid

​ 数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个;释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏。通过数据库连接池能明显提高对数据库操作的性能。在Java应用程序开发中,常用的连接池有DBCP、C3P0、Proxool等。

​ Spring Boot默认提供了若干种可用的连接池,默认的数据源是:org.apache.tomcat.jdbc.pool.DataSource。而Druid是阿里系提供的一个开源连接池,除在连接池之外,Druid还提供了非常优秀的数据库监控和扩展功能。接下来,我们就来讲解如何实现Spring Boot与Druid连接池的集成。

​ Druid是阿里开源的一个JDBC应用组件, 其包括三部分:

  • DruidDriver: 代理Driver,能够提供基于Filter-Chain模式的插件体系。
  • DruidDataSource: 高效可管理的数据库连接池。
  • SQLParser: 实用的SQL语法分析

    通过Druid连接池中间件, 我们可以实现:

  • 可以监控数据库访问性能,Druid内置提供了一个功能强大的StatFilter插件,能够详细统计SQL的执行性能,这对于线上分析数据库访问性能有帮助。
  • 替换传统的DBCP和C3P0连接池中间件。Druid提供了一个高效、功能强大、可扩展性好的数据库连接池。
  • 数据库密码加密。直接把数据库密码写在配置文件中,容易导致安全问题。DruidDruiver和DruidDataSource都支持PasswordCallback。
  • SQL执行日志,Druid提供了不同的LogFilter,能够支持Common-Logging、Log4j和JdkLog,你可以按需要选择相应的LogFilter,监控你应用的数据库访问情况。
  • 扩展JDBC,如果你要对JDBC层有编程的需求,可以通过Druid提供的Filter-Chain机制,很方便编写JDBC层的扩展插件。

更多详细信息参考官方文档:https://github.com/alibaba/dr...

集成实践

Spring Cloud与Spring Boot的版本如下

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <!--版本对应关系:https://start.spring.io/actuator/info-->
        <spring-boot.version>2.2.1.RELEASE</spring-boot.version>
        <spring-cloud.version>Hoxton.SR10</spring-cloud.version>
        <spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
        <java.version>1.8</java.version>
        <spring-mybatis-plus.version>3.4.0</spring-mybatis-plus.version>
        <druid.version>1.1.9</druid.version>
    </properties>

添加Druid的依赖

       <!-- Druid引入 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid.version}</version>
        </dependency>

Nacos上添加对应项目的配置

spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://***.***.***.***:3306/nbgls?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
      username: ***
      password: ***
      filters: stat,wall,log4j,config
      max-active: 100
      initial-size: 1
      max-wait: 60000
      min-idle: 1
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      validation-query: select 'x'
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      pool-prepared-statements: true
      max-open-prepared-statements: 50
      max-pool-prepared-statement-per-connection-size: 20
参数说明:

- spring.datasource.druid.max-active  最大连接数 
- spring.datasource.druid.initial-size  初始化大小 
- spring.datasource.druid.min-idle  最小连接数 
- spring.datasource.druid.max-wait  获取连接等待超时时间 
- spring.datasource.druid.time-between-eviction-runs-millis  间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 
- spring.datasource.druid.min-evictable-idle-time-millis  一个连接在池中最小生存的时间,单位是毫秒 
- spring.datasource.druid.filters=config,stat,wall,log4j  配置监控统计拦截的filters,去掉后监控界面SQL无法进行统计,’wall’用于防火墙

配置DataSourceConfig注册druidDataSource

DruidDataSourceProperties.java

@Data
@RefreshScope
@Component
@ConfigurationProperties(prefix = "spring.datasource.druid")
public class DruidDataSourceProperties {

    /**
     * 读取配置文件中数据库的连接信息
     */
    /**
     * 驱动名称
     */
    private String driverClassName;
    /**
     * 地址
     */
    private String url;
    /**
     * 用户名
     */
    private String username;
    /**
     * 密码
     */
    private String password;

    // jdbc connection pool
    private int initialSize;
    private int minIdle;
    private int maxActive = 100;
    private long maxWait;
    private long timeBetweenEvictionRunsMillis;
    private long minEvictableIdleTimeMillis;
    private String validationQuery;
    private boolean testWhileIdle;
    private boolean testOnBorrow;
    private boolean testOnReturn;
    private boolean poolPreparedStatements;
    private int maxPoolPreparedStatementPerConnectionSize;
    // filter
    private String filters;
}

DataSourceConfig.java

@Configuration
@EnableConfigurationProperties({DruidDataSourceProperties.class})
public class DataSourceConfig {

    private static final Logger LOGGER = LoggerFactory.getLogger(DataSourceConfig.class);

    @Autowired
    private DruidDataSourceProperties druidDataSourceProperties;

    @Bean
    public DataSource druidDataSource() {
        LOGGER.info("==================================== InitDatabaseConfig -> dataSource -> 开始创建数据库 ====================================");
        // 数据库连接对象
        Connection connection = null;
        Statement statement = null;
        String url = druidDataSourceProperties.getUrl();
        String driverClassName = druidDataSourceProperties.getDriverClassName();
        String username = druidDataSourceProperties.getUsername();
        String password = druidDataSourceProperties.getPassword();
        try {

            // 如果尝试去连接不存在的数据库会报错,所以这里连接的时候不带数据库名称
            String connectionUrl = url.replace(("/" + (url.substring(0, url.indexOf("?"))).substring(((url.substring(0, url.indexOf("?"))).lastIndexOf("/")) + 1)), "");
            // 从连接地址中截取出数据库名称
            String databaseName = (url.substring(0, url.indexOf("?"))).substring(((url.substring(0, url.indexOf("?"))).lastIndexOf("/")) + 1);

            // 设置驱动
            Class.forName(driverClassName);
            // 连接数据库
            connection = DriverManager.getConnection(connectionUrl, username, password);
            statement = connection.createStatement();

            // 创建数据库
            statement.executeUpdate("create database if not exists `" + databaseName + "` default character set utf8mb4 COLLATE utf8mb4_general_ci");

        }catch (Exception e) {
            e.printStackTrace();
            LOGGER.info("==================================== InitDatabaseConfig -> dataSource -> 创建数据库出错:" + e.getMessage() + " ====================================");
        }finally {
            try {
                // 关闭连接
                statement.close();
                connection.close();
            }catch (SQLException e) {
                LOGGER.info("==================================== InitDatabaseConfig -> dataSource -> 关闭数据库出错:" + e.getMessage() + " ====================================");
            }
            LOGGER.info("==================================== InitDatabaseConfig -> dataSource -> 创建数据库结束 ====================================");
        }

        // 创建数据源
        DruidDataSource druidDataSource = new DruidDataSource();
        // 设置数据源
        druidDataSource.setDriverClassName(driverClassName);
        druidDataSource.setUrl(url);
        druidDataSource.setUsername(username);
        druidDataSource.setPassword(password);
        druidDataSource.setInitialSize(druidDataSourceProperties.getInitialSize());
        druidDataSource.setMinIdle(druidDataSourceProperties.getMinIdle());
        druidDataSource.setMaxActive(druidDataSourceProperties.getMaxActive());
        druidDataSource.setMaxWait(druidDataSourceProperties.getMaxWait());
        druidDataSource.setTimeBetweenEvictionRunsMillis(druidDataSourceProperties.getTimeBetweenEvictionRunsMillis());
        druidDataSource.setMinEvictableIdleTimeMillis(druidDataSourceProperties.getMinEvictableIdleTimeMillis());
        druidDataSource.setValidationQuery(druidDataSourceProperties.getValidationQuery());
        druidDataSource.setTestWhileIdle(druidDataSourceProperties.isTestWhileIdle());
        druidDataSource.setTestOnBorrow(druidDataSourceProperties.isTestOnBorrow());
        druidDataSource.setTestOnReturn(druidDataSourceProperties.isTestOnReturn());
        druidDataSource.setPoolPreparedStatements(druidDataSourceProperties.isPoolPreparedStatements());
        druidDataSource.setMaxPoolPreparedStatementPerConnectionSize(druidDataSourceProperties.getMaxPoolPreparedStatementPerConnectionSize());
        try {
            druidDataSource.setFilters(druidDataSourceProperties.getFilters());
            druidDataSource.init();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        // 返回数据源
        return druidDataSource;
    }

    /**
     * 注册Servlet信息, 配置监控视图
     *
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    public ServletRegistrationBean<Servlet> druidServlet() {
        ServletRegistrationBean<Servlet> servletRegistrationBean = new ServletRegistrationBean<Servlet>(new StatViewServlet(), "/druid/*");

        //白名单:
        servletRegistrationBean.addInitParameter("allow","127.0.0.1");
        //IP黑名单 (存在共同时,deny优先于allow) : 如果满足deny的话提示:Sorry, you are not permitted to view this page.
       // servletRegistrationBean.addInitParameter("deny","192.168.1.119");
        //登录查看信息的账号密码, 用于登录Druid监控后台
        servletRegistrationBean.addInitParameter("loginUsername", "admin");
        servletRegistrationBean.addInitParameter("loginPassword", "admin");
        //是否能够重置数据.
        servletRegistrationBean.addInitParameter("resetEnable", "true");
        return servletRegistrationBean;

    }

    /**
     * 注册Filter信息, 监控拦截器
     *
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    public FilterRegistrationBean<Filter> filterRegistrationBean() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<Filter>();
        filterRegistrationBean.setFilter(new WebStatFilter());
        filterRegistrationBean.addUrlPatterns("/*");
        filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
        return filterRegistrationBean;
    }

}

添加 log4j 依赖

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

在 resources 目录下,新建一个 log4j.properties 参数配置文件

#############
# 输出到控制台
#############

# log4j.rootLogger日志输出类别和级别:只输出不低于该级别的日志信息DEBUG < INFO < WARN < ERROR < FATAL
# WARN:日志级别     CONSOLE:输出位置自己定义的一个名字       logfile:输出位置自己定义的一个名字
log4j.rootLogger=WARN,CONSOLE,logfile
# 配置CONSOLE输出到控制台
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender 
# 配置CONSOLE设置为自定义布局模式
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout 
# 配置CONSOLE日志的输出格式  [frame] 2019-08-22 22:52:12,000  %r耗费毫秒数 %p日志的优先级 %t线程名 %C所属类名通常为全类名 %L代码中的行号 %x线程相关联的NDC %m日志 %n换行
log4j.appender.CONSOLE.layout.ConversionPattern=[frame] %d{yyyy-MM-dd HH:mm:ss,SSS} - %-4r %-5p [%t] %C:%L %x - %m%n

################
# 输出到日志文件中
################

# 配置logfile输出到文件中 文件大小到达指定尺寸的时候产生新的日志文件
log4j.appender.logfile=org.apache.log4j.RollingFileAppender
# 保存编码格式
log4j.appender.logfile.Encoding=UTF-8
# 输出文件位置此为项目根目录下的logs文件夹中
log4j.appender.logfile.File=logs/root.log
# 后缀可以是KB,MB,GB达到该大小后创建新的日志文件
log4j.appender.logfile.MaxFileSize=10MB
# 设置滚定文件的最大值3 指可以产生root.log.1、root.log.2、root.log.3和root.log四个日志文件
log4j.appender.logfile.MaxBackupIndex=3  
# 配置logfile为自定义布局模式
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %F %p %m%n

##########################
# 对不同的类输出不同的日志文件
##########################

# club.bagedate包下的日志单独输出
log4j.logger.club.bagedate=DEBUG,bagedate
# 设置为false该日志信息就不会加入到rootLogger中了
log4j.additivity.club.bagedate=false
# 下面就和上面配置一样了
log4j.appender.bagedate=org.apache.log4j.RollingFileAppender
log4j.appender.bagedate.Encoding=UTF-8
log4j.appender.bagedate.File=logs/bagedate.log
log4j.appender.bagedate.MaxFileSize=10MB
log4j.appender.bagedate.MaxBackupIndex=3
log4j.appender.bagedate.layout=org.apache.log4j.PatternLayout
log4j.appender.bagedate.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %F %p %m%n

启动后访问http://localhost:9530/druid/l...

MyBatis Plus

  • Mybatis-plus是对Mybatis框架的二次封装和扩展纯正血统
  • 完全继承原生 Mybatis 的所有特性最少依赖,仅仅依赖Mybatis以及Mybatis-Spring性能损耗小
  • 启动即会自动注入基本CURD ,性能无损耗,直接面向对象操作自动热加载,Mapper对应的xml可以热加载,大大减少重启Web服务器时间,提升开发效率
  • 自带Sql性能分析插件,开发测试时,能有效解决慢查询全局拦截
  • 提供全表delete、update操作智能分析阻断避免Sql注入,内置Sql注入内容剥离器,预防Sql注入攻击

集成实践

配置

(不包含代码生成)

添加MyBatis Plus的依赖

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${spring-mybatis-plus.version}</version>
        </dependency>

Nacos上添加MyBatis Plus相关的配置

mybatis-plus:
  mapperLocations: classpath*:com/example/admin/mapper/xml/*Mapper.xml
  type-aliases-package: com.example.admin.mapper
  global-config:
    # 逻辑删除配置
    db-config:
      # 删除前
      logic-not-delete-value: 1
      # 删除后
      logic-delete-value: 0

配置MyBatisPlusConfig

@Configuration
@EnableTransactionManagement
@MapperScan(basePackages = {"com.example.auth.mapper"})
@RefreshScope
public class MyBatisPlusConfig {

    @Value("${mybatis-plus.mapper-locations}")
    private String configLocation;

    @Value("${mybatis-plus.type-aliases-package}")
    private String typeAliasesPackage;

    @Autowired
    private ResourceLoader resourceLoader = new DefaultResourceLoader();

    /**
     * 乐观锁插件 MP3.3.0之后更改
     * 分页插件
     */
    @Bean
    public MybatisPlusInterceptor optimisticLockerInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //乐观锁
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        //分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }
}
测试

新建实体类UserEntity.java

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
@TableName("sys_user")
@ApiModel(value = "用户信息")
public class UserEntity extends BaseEntity {

    /**
     * 主键ID
     * default value: null
     */
    @TableId(value = "user_id", type = IdType.AUTO)
    @ApiModelProperty(value = "主键ID")
    private Long userId;

    /**
     * 姓名
     * default value: null
     */
    private String name;

    /**
     * 用户角色,分为普通用户/管理员
     * default value: null
     */
    @ApiModelProperty(value = "用户类别,分为普通用户(ROLE_USER)/管理员(ROLE_ADMIN)")
    private String role;

    /**
     * 用户账号
     * default value: null
     */
    @ApiModelProperty(value = "用户名")
    private String userName;

    /**
     * 邮箱
     * default value: null
     */
    @ApiModelProperty(value = "邮箱")
    private String email;

    /**
     * 电话号码
     * default value: null
     */
    @ApiModelProperty(value = "电话号码")
    private String telephone;

    /**
     * 用户性别,F/M
     * default value: null
     */
    @ApiModelProperty(value = "用户性别,F/M")
    private String gender;

    /**
     * 头像存储路径
     * default value: null
     */
    @ApiModelProperty(value = "头像存储路径", hidden = true)
    private String avatar;

    /**
     * 密码
     * default value: null
     */
    @ApiModelProperty(value = "密码", hidden = true)
    private String password;

    /**
     * 是否启用,默认为Y
     * default value: null
     */
    @ApiModelProperty(value = "是否启用,默认为Y")
    private String enabled;


    /**
     * 用户标签信息,以;分割
     * default value: null
     */
    @ApiModelProperty(value = "用户标签信息,以;分割")
    private String tags;
}

BaseEntity.java

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class BaseEntity extends Model {
    /**
     * 创建人
     * default value: null
     */
    private Long createBy;

    /**
     * 创建日期
     * insert时自动填充
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;

    /**
     * 修改人
     * default value: null
     */
    private Long updateBy;

    /**
     * 修改日期
     * update时自动填充
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(fill = FieldFill.INSERT_UPDATE, update="NOW()")
    private Date updateTime;

    @Version
    @TableField(fill = FieldFill.INSERT, update="%s+1")
    private Integer version;
}

关于MyBatis Plus注解的使用,可以参考官网https://baomidou.com/guide/an...

这里要说明的是TableField的fill自动填充功能

public enum FieldFill {
    /**
     * 默认不处理
     */
    DEFAULT,
    /**
     * 插入填充字段
     */
    INSERT,
    /**
     * 更新填充字段
     */
    UPDATE,
    /**
     * 插入和更新填充字段
     */
    INSERT_UPDATE
}
  • 填充原理是直接给entity的属性设置值!!!
  • 注解则是指定该属性在对应情况下必有值,如果无值则入库会是null
  • MetaObjectHandler提供的默认方法的策略均为:如果属性有值则不覆盖,如果填充值为null则不填充
  • 字段必须声明TableField注解,属性fill选择对应策略,该声明告知Mybatis-Plus需要预留注入SQL字段
  • 填充处理器MyMetaObjectHandler在 Spring Boot 中需要声明@Component@Bean注入
  • 要想根据注解FieldFill.xxx字段名以及字段类型来区分必须使用父类的strictInsertFill或者strictUpdateFill方法
  • 不需要根据任何来区分可以使用父类的fillStrategy方法

这里自定义MyMetaObjectHandler对updateTime,version等字段做处理

@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("start insert fill ....");
        this.strictInsertFill(metaObject, "create_time", LocalDateTime.class, LocalDateTime.now());
        // 或者
        this.strictInsertFill(metaObject, "version", Integer.class, 1);
        this.strictInsertFill(metaObject, "update_time", LocalDateTime.class, LocalDateTime.now());
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("start update fill ....");
        this.strictUpdateFill(metaObject, "update_time", LocalDateTime.class, LocalDateTime.now());
    }
}

新建mapper

@Mapper
public interface UserMapper extends BaseMapper<UserEntity> {
}

新建service

public interface IUserService extends IService<UserEntity> {
}
@Service("userService")
public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> implements IUserService {
}

即可使用


花花呀
363 声望21 粉丝

学无止境 做有灵魂的程序员