基于SpringBoot搭建应用开发框架2

230 阅读23分钟

书接上回 基于SpringBoot搭建应用开发框架1

三、集成MyBatis,实现基础Mapper和Service

1、添加JDBC、配置数据源

添加spring-boot-starter-jdbc以支持JDBC访问数据库,然后添加MySql的JDBC驱动mysql-connector-java;

在application.properties里配置mysql的数据库驱动

之后在application-dev.properties里配置开发环境数据库的连接信息,添加之后,Springboot就会自动配置数据源了。

2、集成MyBatis

MyBatis官方为了方便Springboot集成MyBatis,专门提供了一个符合Springboot规范的starter项目,即mybatis-spring-boot-starter。

在application.properties里添加mybatis映射配置:

3、添加MyBatis通用Mapper

通用Mapper可以极大的简化开发,极其方便的进行单表的增删改查。

关于通用Mapper,参考网站地址:

  MyBatis通用Mapper

  MyBatis 相关工具

之后,在core.base下创建自定义的Mapper,按需选择接口。 之后,在core.base下创建自定义的Mapper,按需选择接口。

具体可参考:根据需要自定义接口

package com.lyyzoo.core.base;

import tk.mybatis.mapper.common.BaseMapper;
import tk.mybatis.mapper.common.ConditionMapper;
import tk.mybatis.mapper.common.IdsMapper;
import tk.mybatis.mapper.common.special.InsertListMapper;

/**
 *
 * BaseMapper
 *
 * @name BaseMapper
 * @version 1.0
 * @author bojiangzhou 2017-12-31
 */
public interface Mapper<T> extends BaseMapper<T>, ConditionMapper<T>, IdsMapper<T>, InsertListMapper<T> {

}

定义好基础Mapper后,就具有下图中的基本通用方法了。每个实体类对应的*Mapper继承Mapper来获得基本的增删改查的通用方法。

在application.properties里配置自定义的基础Mapper

4、添加分页插件PageHelper

参考地址:

  MyBatis 分页插件 - PageHelper

  分页插件使用方法

分页插件配置,一般情况下,不需要做任何配置。

之后,我们就可以在代码中使用 PageHelper.startPage(1, 10) 对紧随其后的一个查询进行分页查询,非常方便。

5、配置自动扫描Mapper

在config下创建MyBatisConfig配置文件,通过mapperScannerConfigurer方法配置自动扫描Mapper文件。

package com.lyyzoo.core.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import tk.mybatis.spring.mapper.MapperScannerConfigurer;

/**
 * MyBatis相关配置.
 *
 * @version 1.0
 * @author bojiangzhou 2018-01-07
 */
@Configuration
public class MyBatisConfig {

    /**
     * Mapper扫描配置. 自动扫描将Mapper接口生成代理注入到Spring.
     */
    @Bean
    public static MapperScannerConfigurer mapperScannerConfigurer() {
        MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
        // 注意这里的扫描路径: 1.不要扫描到自定义的Mapper; 2.定义的路径不要扫描到tk.mybatis.mapper(如定义**.mapper).
        // 两个做法都会导致扫描到tk.mybatis的Mapper,就会产生重复定义的报错.
        mapperScannerConfigurer.setBasePackage("**.lyyzoo.**.mapper");
        return mapperScannerConfigurer;
    }

}

注意这里的 MapperScannerConfigurer 是tk.mybatis.spring.mapper.MapperScannerConfigurer,而不是org.mybatis,否则使用通用Mapper的方法时会报类似下面的这种错误

6、定义基础Service

一般来说,我们不能在Controller中直接访问Mapper,因此我们需要加上Service,通过Service访问Mapper。

首先定义基础Service接口,根据Mapper定义基本的增删改查接口方法。

package com.lyyzoo.core.base;

import java.util.List;

/**
 * Service 基础通用接口
 *
 * @name BaseService
 * @version 1.0
 * @author bojiangzhou 2017-12-31
 */
public interface Service<T> {

    //
    // insert
    // ----------------------------------------------------------------------------------------------------
    /**
     * 保存一个实体,null的属性也会保存,不会使用数据库默认值
     *
     * @param record
     * @return
     */
    T insert(T record);

    /**
     * 批量插入,null的属性也会保存,不会使用数据库默认值
     *
     * @param recordList
     * @return
     */
    List<T> insert(List<T> recordList);

    /**
     * 保存一个实体,null的属性不会保存,会使用数据库默认值
     *
     * @param record
     * @return
     */
    T insertSelective(T record);

    /**
     * 批量插入,null的属性不会保存,会使用数据库默认值
     *
     * @param recordList
     * @return
     */
    List<T> insertSelective(List<T> recordList);

    //
    // update
    // ----------------------------------------------------------------------------------------------------
    /**
     * 根据主键更新实体全部字段,null值会被更新
     *
     * @param record
     * @return
     */
    T update(T record);

    /**
     * 批量更新,根据主键更新实体全部字段,null值会被更新
     *
     * @param recordList
     * @return
     */
    List<T> update(List<T> recordList);

    /**
     * 根据主键更新属性不为null的值
     *
     * @param record
     * @return
     */
    T updateSelective(T record);

    /**
     * 批量更新,根据主键更新属性不为null的值
     *
     * @param recordList
     * @return
     */
    List<T> updateSelective(List<T> recordList);

    //
    // delete
    // ----------------------------------------------------------------------------------------------------
    /**
     * 根据主键删除
     *
     * @param id id不能为空
     * @return
     */
    int delete(Long id);

    /**
     * 根据主键字符串进行删除,类中只有存在一个带有@Id注解的字段
     *
     * @param ids 类似1,2,3
     */
    int delete(String ids);

    /**
     * 根据主键删除多个实体,ID数组
     *
     * @param ids 类似[1,2,3],不能为空
     */
    int delete(Long[] ids);

    /**
     * 根据实体属性作为条件进行删除
     *
     * @param record
     * @return
     */
    int delete(T record);

    /**
     * 根据主键删除多个实体
     *
     * @param recordList
     * @return
     */
    int delete(List<T> recordList);

    //
    // insert or update or delete
    // ----------------------------------------------------------------------------------------------------
    /**
     * 根据实体的operate决定哪种操作. null的属性也会保存,不会使用数据库默认值
     *
     * @param record
     * @return
     */
    T persist(T record);

    /**
     * 批量操作.根据实体的operate决定哪种操作. null的属性也会保存,不会使用数据库默认值
     *
     * @param recordList
     * @return
     */
    List<T> persist(List<T> recordList);

    /**
     * 根据实体的operate决定哪种操作. 根据主键更新属性不为null的值
     *
     * @param record
     * @return
     */
    T persistSelective(T record);

    /**
     * 批量操作.根据实体的operate决定哪种操作. 根据主键更新属性不为null的值
     *
     * @param recordList
     * @return
     */
    List<T> persistSelective(List<T> recordList);


    //
    // select
    // ----------------------------------------------------------------------------------------------------
    /**
     * 根据主键查询
     *
     * @param id 不能为空
     * @return
     */
    T get(Long id);

    /**
     * 根据实体中的属性进行查询,只能有一个返回值,有多个结果是抛出异常
     *
     * @param record
     * @return
     */
    T get(T record);

    /**
     * 根据字段和值查询 返回一个
     * @param key 不能为空
     * @param value 不能为空
     * @return
     */
    T get(String key, Object value);


    /**
     * 根据主键字符串进行查询
     *
     * @param ids 如 "1,2,3,4"
     * @return
     */
    List<T> select(String ids);

    /**
     * 根据实体中的属性值进行查询
     *
     * @param record
     * @return
     */
    List<T> select(T record);

    /**
     * 根据属性和值查询
     *
     * @param key
     * @param value
     * @return
     */
    List<T> select(String key, Object value);

    /**
     * 根据实体中的属性值进行分页查询
     *
     * @param record
     * @param pageNum
     * @param pageSize
     * @return
     */
    List<T> select(T record, int pageNum, int pageSize);

    /**
     * 查询全部结果
     *
     * @return
     */
    List<T> selectAll();

    /**
     * 根据实体中的属性查询总数
     *
     * @param record
     * @return
     */
    int count(T record);

}

然后是实现类BaseService,以后的开发中,Service接口实现Service,Service实现类继承BaseService。

package com.lyyzoo.core.base;

import com.github.pagehelper.PageHelper;
import com.lyyzoo.core.constants.Constants;
import com.lyyzoo.core.exception.UpdateFailedException;
import com.lyyzoo.core.util.Reflections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import javax.annotation.PostConstruct;
import javax.persistence.Id;
import java.lang.reflect.Field;
import java.util.List;

/**
 * 基础Service实现类
 *
 * @version 1.0
 * @author bojiangzhou 2018-01-04
 */
public abstract class BaseService<T> implements Service<T> {

    @Autowired
    private Mapper<T> mapper;

    private Class<T> entityClass;

    @SuppressWarnings("unchecked")
    @PostConstruct
    public void init() {
        this.entityClass = Reflections.getClassGenericType(getClass());
    }

    //
    // insert
    // ----------------------------------------------------------------------------------------------------
    @Transactional(rollbackFor = Exception.class)
    public T insert(T record) {
        mapper.insert(record);
        return record;
    }

    @Transactional(rollbackFor = Exception.class)
    public List<T> insert(List<T> recordList) {
        mapper.insertList(recordList);
        return recordList;
    }

    @Transactional(rollbackFor = Exception.class)
    public T insertSelective(T record) {
        mapper.insertSelective(record);
        return record;
    }

    @Transactional(rollbackFor = Exception.class)
    public List<T> insertSelective(List<T> recordList) {
        // 由于Mapper暂未提供Selective的批量插入,此处循环查询. 当然也可参考InsertListMapper自己实现.
        for(T record : recordList){
            mapper.insertSelective(record);
        }
        return recordList;
    }

    //
    // update
    // ----------------------------------------------------------------------------------------------------
    @Transactional(rollbackFor = Exception.class)
    public T update(T record) {
        int count = mapper.updateByPrimaryKey(record);
        checkUpdate(count, record);
        return record;
    }

    @Transactional(rollbackFor = Exception.class)
    public List<T> update(List<T> recordList) {
        // Mapper暂未提供批量更新,此处循实现
        for(T record : recordList){
            int count = mapper.updateByPrimaryKey(record);
            checkUpdate(count, record);
        }
        return recordList;
    }

    @Transactional(rollbackFor = Exception.class)
    public T updateSelective(T record) {
        int count = mapper.updateByPrimaryKeySelective(record);
        checkUpdate(count, record);
        return record;
    }

    @Transactional(rollbackFor = Exception.class)
    public List<T> updateSelective(List<T> recordList) {
        // Mapper暂未提供批量更新,此处循实现
        for(T record : recordList){
            int count = mapper.updateByPrimaryKeySelective(record);
            checkUpdate(count, record);
        }
        return recordList;
    }

    //
    // delete
    // ----------------------------------------------------------------------------------------------------
    @Transactional(rollbackFor = Exception.class)
    public int delete(Long id) {
        return mapper.deleteByPrimaryKey(id);
    }

    @Transactional(rollbackFor = Exception.class)
    public int delete(Long[] ids) {
        int count = 0;
        for(Long id : ids){
            mapper.deleteByPrimaryKey(id);
            count++;
        }
        return count;
    }

    @Transactional(rollbackFor = Exception.class)
    public int delete(T record) {
        return mapper.delete(record);
    }

    @Transactional(rollbackFor = Exception.class)
    public int delete(List<T> recordList) {
        int count = 0;
        for(T record : recordList){
            mapper.delete(record);
            count++;
        }
        return count;
    }

    //
    // all operate. insert or update or delete
    // ----------------------------------------------------------------------------------------------------
    @Transactional(rollbackFor = Exception.class)
    public T persist(T record) {
        BaseDTO dto = (BaseDTO) record;
        Assert.notNull(dto.get_operate(), "_operate not be null.");
        switch (dto.get_operate()) {
            case Constants.Operation.ADD:
                insert(record);
                break;
            case Constants.Operation.UPDATE:
                update(record);
                break;
            case Constants.Operation.DELETE:
                delete(record);
                break;
            default:
                break;
        }
        dto.set_operate(null);
        return record;
    }

    @Transactional(rollbackFor = Exception.class)
    public List<T> persist(List<T> recordList) {
        for(T record : recordList){
            BaseDTO dto = (BaseDTO) record;
            Assert.notNull(dto.get_operate(), "_operate not be null.");
            switch (dto.get_operate()) {
                case Constants.Operation.ADD:
                    insert(record);
                    break;
                case Constants.Operation.UPDATE:
                    update(record);
                    break;
                case Constants.Operation.DELETE:
                    delete(record);
                    break;
                default:
                    break;
            }
            dto.set_operate(null);
        }
        return recordList;
    }

    @Transactional(rollbackFor = Exception.class)
    public T persistSelective(T record) {
        BaseDTO dto = (BaseDTO) record;
        Assert.notNull(dto.get_operate(), "_operate not be null.");
        switch (dto.get_operate()) {
            case Constants.Operation.ADD:
                insertSelective(record);
                break;
            case Constants.Operation.UPDATE:
                updateSelective(record);
                break;
            case Constants.Operation.DELETE:
                delete(record);
                break;
            default:
                break;
        }
        return record;
    }

    @Transactional(rollbackFor = Exception.class)
    public List<T> persistSelective(List<T> recordList) {
        for(T record : recordList){
            BaseDTO dto = (BaseDTO) record;
            Assert.notNull(dto.get_operate(), "_operate not be null.");
            switch (dto.get_operate()) {
                case Constants.Operation.ADD:
                    insertSelective(record);
                    break;
                case Constants.Operation.UPDATE:
                    updateSelective(record);
                    break;
                case Constants.Operation.DELETE:
                    delete(record);
                    break;
                default:
                    break;
            }
        }
        return recordList;
    }

    //
    // select
    // ----------------------------------------------------------------------------------------------------
    public T get(Long id) {
        T entity = null;
        try {
            entity = entityClass.newInstance();
            Field idField = Reflections.getFieldByAnnotation(entityClass, Id.class);
            idField.set(entity, id);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return mapper.selectByPrimaryKey(entity);
    }

    public T get(T record) {
        return mapper.selectOne(record);
    }

    public T get(String key, Object value) {
        T entity = null;
        try {
            entity = entityClass.newInstance();
            Field field = Reflections.getField(entityClass, key);
            field.set(entity, value);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return mapper.selectOne(entity);
    }

    public List<T> select(String ids) {
        return mapper.selectByIds(ids);
    }

    public List<T> select(T record) {

        return mapper.select(record);
    }

    public List<T> select(String key, Object value) {
        T entity = null;
        try {
            entity = entityClass.newInstance();
            Field field = Reflections.getField(entityClass, key);
            field.set(entity, value);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return mapper.select(entity);
    }

    public List<T> select(T record, int pageNum, int pageSize) {
        PageHelper.startPage(pageNum, pageSize);
        return mapper.select(record);
    }

    public List<T> selectAll() {
        return mapper.selectAll();
    }

    public int count(T record) {
        return mapper.selectCount(record);
    }

    /**
     * 检查乐观锁<br>
     * 更新失败时,抛出 UpdateFailedException 异常
     *
     * @param updateCount update,delete 操作返回的值
     * @param record 操作参数
     */
    protected void checkUpdate(int updateCount, Object record) {
        if (updateCount == 0 && record instanceof BaseDTO) {
            BaseDTO baseDTO = (BaseDTO) record;
            if (baseDTO.getVersion() != null) {
                throw new UpdateFailedException();
            }
        }
    }

}

BaseService的实现用到了反射工具类Reflections:

package com.lyyzoo.core.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

/**
 * 反射工具类.
 *
 * @version 1.0
 * @author bojiangzhou 2018-01-06
 */

public abstract class Reflections {

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

    /**
     * 通过反射, 获得Class定义中声明的泛型参数的类型, 注意泛型必须定义在父类处. 如无法找到, 返回Object.class.
     *
     * @param clazz class类
     *
     * @return the 返回第一个声明的泛型类型. 如果没有,则返回Object.class
     */
    @SuppressWarnings("unchecked")
    public static Class getClassGenericType(final Class clazz) {
        return getClassGenericType(clazz, 0);
    }

    /**
     * 通过反射, 获得Class定义中声明的父类的泛型参数的类型. 如无法找到, 返回Object.class.
     *
     * @param clazz class类
     *
     * @param index 获取第几个泛型参数的类型,默认从0开始,即第一个
     *
     * @return 返回第index个泛型参数类型.
     */
    public static Class getClassGenericType(final Class clazz, final int index) {
        Type genType = clazz.getGenericSuperclass();

        if (!(genType instanceof ParameterizedType)) {
            return Object.class;
        }

        Type[] params = ((ParameterizedType) genType).getActualTypeArguments();

        if (index >= params.length || index < 0) {
            logger.warn("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: " + params.length);
            return Object.class;
        }
        if (!(params[index] instanceof Class)) {
            logger.warn(clazz.getSimpleName() + " not set the actual class on superclass generic parameter");
            return Object.class;
        }

        return (Class) params[index];
    }

    /**
     * 根据注解类型获取实体的Field
     *
     * @param entityClass 实体类型
     *
     * @param annotationClass 注解类型
     *
     * @return 返回第一个有该注解类型的Field,如果没有则返回null.
     */
    @SuppressWarnings("unchecked")
    public static Field getFieldByAnnotation(Class entityClass, Class annotationClass) {
        Field[] fields = entityClass.getDeclaredFields();
        for (Field field : fields) {
            if (field.getAnnotation(annotationClass) != null) {
                makeAccessible(field);
                return field;
            }
        }
        return null;
    }

    /**
     * 获取实体的字段
     *
     * @param entityClass 实体类型
     *
     * @param fieldName 字段名称
     *
     * @return 该字段名称对应的字段,如果没有则返回null.
     */
    public static Field getField(Class entityClass, String fieldName){
        try {
            Field field = entityClass.getDeclaredField(fieldName);
            makeAccessible(field);
            return field;
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
        return null;
    }


    /**
     * 改变private/protected的成员变量为public.
     */
    public static void makeAccessible(Field field) {
        if (!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())) {
            field.setAccessible(true);
        }
    }

}

7、获取AOP代理

Spring 只要引入aop则是默认开启事务的,一般我们只要在需要事务管理的地方加上@Transactional注解即可支持事务,一般我们会加在Service的类或者具体的增加、删除、更改的方法上。

我这里要说的是获取代理的问题。Service的事务管理是AOP实现的,AOP的实现用的是JDK动态代理或CGLIB动态代理。所以,如果你想在你的代理方法中以 this 调用当前接口的另一个方法,另一个方法的事务是不会起作用的。因为事务的方法是代理对象的,而 this 是当前类对象,不是一个代理对象,自然事务就不会起作用了。这是我在不久前的开发中遇到的实际问题,我自定义了一个注解,加在方法上,使用AspectJ来拦截该注解,却没拦截到,原因就是这个方法是被另一个方法以 this 的方式调用的,所以AOP不能起作用。

更详细的可参考:Spring AOP无法拦截内部方法调用

所以添加一个获取自身代理对象的接口,以方便获取代理对象来操作当前类方法。Service接口只需要继承该接口,T为接口本身即可,就可以通过self()获取自身的代理对象了。

package com.lyyzoo.core.base;

import org.springframework.aop.framework.AopContext;

/**
 * 获取代理对象本身.
 */
public interface ProxySelf<T> {
    /**
     * 取得当前对象的代理.
     *
     * @return 代理对象,如果未被代理,则抛出 IllegalStateException
     */
    @SuppressWarnings("unchecked")
    default T self() {
        return (T) AopContext.currentProxy();
    }
}

还需要开启开启 exposeProxy = true,暴露代理对象,否则 AopContext.currentProxy() 会抛出异常。

8、数据持久化测试

① 实体映射

实体类按照如下规则和数据库表进行转换,注解全部是JPA中的注解:

  • 表名默认使用类名,驼峰转下划线(只对大写字母进行处理),如UserInfo默认对应的表名为user_info
  • 表名可以使@Table(name = "tableName")进行指定,对不符合第一条默认规则的可以通过这种方式指定表名。
  • 字段默认和@Column一样,都会作为表字段,表字段默认为Java对象的Field名字驼峰转下划线形式。
  • 可以使用@Column(name = "fieldName")指定不符合第3条规则的字段名。
  • 使用@Transient注解可以忽略字段,添加该注解的字段不会作为表字段使用,注意,如果没有与表关联,一定要用@Transient标注。
  • 建议一定是有一个@Id注解作为主键的字段,可以有多个@Id注解的字段作为联合主键。
  • 默认情况下,实体类中如果不存在包含@Id注解的字段,所有的字段都会作为主键字段进行使用(这种效率极低)。
  • 由于基本类型,如int作为实体类字段时会有默认值0,而且无法消除,所以实体类中建议不要使用基本类型。

User实体主要加了@Table注解,映射表名;然后在userId上标注主键注解;其它字段如果没加@Transient注解的默认都会作为表字段。

package com.lyyzoo.core.system.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.lyyzoo.core.base.BaseDTO;
import com.lyyzoo.core.util.Dates;

import javax.persistence.*;
import java.util.Date;
import java.util.List;

/**
 * 系统用户
 *
 * @name User
 * @version 1.0
 * @author bojiangzhou 2017-12-31
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
@Table(name = "SYS_USER")
public class User extends BaseDTO {
    private static final long serialVersionUID = -7395431342743009038L;

    /**
     * 用户ID
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @OrderBy("DESC")
    private Long userId;
    /**
     * 用户名
     */
    private String username;
    /**
     * 密码
     */
    private String password;
    /**
     * 昵称
     */
    private String nickname;
    /**
     * 生日
     */
    @JsonFormat(pattern = Dates.Pattern.DATE)
    private Date birthday;
    /**
     * 性别:1-男/0-女
     */
    private Integer sex;
    /**
     * 是否启用:1/0
     */
    private Integer enabled;


    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public Integer getSex() {
        return sex;
    }

    public void setSex(Integer sex) {
        this.sex = sex;
    }

    public Integer getEnabled() {
        return enabled;
    }

    public void setEnabled(Integer enabled) {
        this.enabled = enabled;
    }

}

② 创建表结构

CREATE TABLE `sys_user` (
  `USER_ID` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '表ID,主键,供其他表做外键',
  `USERNAME` varchar(30) NOT NULL COMMENT '用户名',
  `PASSWORD` varchar(100) NOT NULL COMMENT '密码',
  `NICKNAME` varchar(30) NOT NULL COMMENT '用户名称',
  `BIRTHDAY` date DEFAULT NULL COMMENT '生日',
  `SEX` int(1) DEFAULT NULL COMMENT '性别:1-男;0-女',
  `ENABLED` int(1) NOT NULL DEFAULT '1' COMMENT '启用标识:1/0',
  `VERSION_NUMBER` int(11) NOT NULL DEFAULT '1' COMMENT '行版本号,用来处理锁',
  `CREATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `CREATE_BY` bigint(11) NOT NULL DEFAULT '-1' COMMENT '创建人',
  `UPDATE_BY` bigint(11) NOT NULL DEFAULT '-1' COMMENT '更新人',
  `UPDATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  `ATTRIBUTE1` varchar(150) DEFAULT NULL,
  `ATTRIBUTE2` varchar(150) DEFAULT NULL,
  `ATTRIBUTE3` varchar(150) DEFAULT NULL,
  `ATTRIBUTE4` varchar(150) DEFAULT NULL,
  `ATTRIBUTE5` varchar(150) DEFAULT NULL,
  `ATTRIBUTE6` varchar(150) DEFAULT NULL,
  `ATTRIBUTE7` varchar(150) DEFAULT NULL,
  `ATTRIBUTE8` varchar(150) DEFAULT NULL,
  `ATTRIBUTE9` varchar(150) DEFAULT NULL,
  `ATTRIBUTE10` varchar(150) DEFAULT NULL,
  PRIMARY KEY (`USER_ID`),
  UNIQUE KEY `USERNAME` (`USERNAME`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='系统用户';

③ 创建UserMapper

在system.mapper下创建UserMapper接口,继承Mapper:

package com.lyyzoo.core.system.mapper;

import com.lyyzoo.core.base.Mapper;
import com.lyyzoo.core.system.dto.User;

/**
 *
 * @name UserMapper
 * @version 1.0
 * @author bojiangzhou 2018-01-06
 */
public interface UserMapper extends Mapper<User> {

}

④ 创建UserService

在system.service下创建UserService接口,只需继承Service接口即可。

package com.lyyzoo.core.system.service;

import com.lyyzoo.core.base.Service;
import com.lyyzoo.core.system.dto.User;

/**
 * 用户Service接口
 *
 * @version 1.0
 * @author bojiangzhou 2018-01-06
 */
public interface UserService extends Service<User> {

}

在system.service.impl下创建UserServiceImpl实现类,继承BaseService类,实现UserService接口。同时加上@Service注解。

package com.lyyzoo.core.system.service.impl;

import org.springframework.stereotype.Service;

import com.lyyzoo.core.base.BaseService;
import com.lyyzoo.core.system.dto.User;
import com.lyyzoo.core.system.service.UserService;

/**
 * 用户Service实现类
 *
 * @version 1.0
 * @author bojiangzhou 2018-01-06
 */
@Service
public class UserServiceImpl extends BaseService<User> implements UserService {

}

⑤ 修改UserController,注入UserService,增加一些测试API

package com.lyyzoo.core.system.controller;

import com.lyyzoo.core.base.BaseController;
import com.lyyzoo.core.base.BaseEnums;
import com.lyyzoo.core.base.Result;
import com.lyyzoo.core.system.dto.User;
import com.lyyzoo.core.system.service.UserService;
import com.lyyzoo.core.util.Results;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.List;

/**
 * 用户Controller
 *
 * @version 1.0
 * @author bojiangzhou 2017-12-31
 */
@RequestMapping
@RestController
public class UserController extends BaseController {

    @Autowired
    private UserService userService;


    @PostMapping("/sys/user/queryAll")
    public Result queryAll(){
        List<User> list = userService.selectAll();
        return Results.successWithData(list, BaseEnums.SUCCESS.code(), BaseEnums.SUCCESS.description());
    }

    @RequestMapping("/sys/user/queryOne/{userId}")
    public Result queryOne(@PathVariable Long userId){
        User user = userService.get(userId);
        return Results.successWithData(user);
    }

    @PostMapping("/sys/user/save")
    public Result save(@Valid @RequestBody User user){
        user = userService.insertSelective(user);
        return Results.successWithData(user);
    }

    @PostMapping("/sys/user/update")
    public Result update(@Valid @RequestBody List<User> user){
        user = userService.persistSelective(user);
        return Results.successWithData(user);
    }

    @RequestMapping("/sys/user/delete")
    public Result delete(User user){
        userService.delete(user);
        return Results.success();
    }

    @RequestMapping("/sys/user/delete/{userId}")
    public Result delete(@PathVariable Long userId){
        userService.delete(userId);
        return Results.success();
    }

}

⑥ 测试结果

查询所有:

批量保存/修改:

9、代码生成器

使用代码生成器来生成基础的代码结构,生成DTO、XML等等。

MyBatis官方提供了代码生成器MyBatis Generator,但一般需要定制化。MyBatis Generator

我这里从网上找了一个使用起来比较方便的界面工具,可生成DTO、Mapper、Mapper.xml,生成之后还需做一些小调整。另需要自己创建对应的Service、Controller。之后有时间再重新定制化一个符合本项目的代码生成器。

mybatis-generator界面工具

四、日志及全局异常处理

在前面的测试中,会发现控制台输出的日志不怎么友好,有很多日志也没有输出,不便于查找排查问题。对于一个应用程序来说日志记录是必不可少的一部分。线上问题追踪,基于日志的业务逻辑统计分析等都离不日志。

先贴出一些参考资料:

  logback 配置详解

  日志组件slf4j介绍及配置详解

  Java常用日志框架介绍

1、日志框架简介

Java有很多常用的日志框架,如Log4j、Log4j 2、Commons Logging、Slf4j、Logback等。有时候你可能会感觉有点混乱,下面简单介绍下。

  • Log4j:Apache Log4j是一个基于Java的日志记录工具,是Apache软件基金会的一个项目。
  • Log4j 2:Apache Log4j 2是apache开发的一款Log4j的升级产品。
  • Commons Logging:Apache基金会所属的项目,是一套Java日志接口。
  • Slf4j:类似于Commons Logging,是一套简易Java日志门面,本身并无日志的实现。(Simple Logging Facade for Java,缩写Slf4j)。
  • Logback:一套日志组件的实现(slf4j阵营)。

Commons Logging和Slf4j是日志门面,提供一个统一的高层接口,为各种loging API提供一个简单统一的接口。log4j和Logback则是具体的日志实现方案。可以简单的理解为接口与接口的实现,调用者只需要关注接口而无需关注具体的实现,做到解耦。

比较常用的组合使用方式是Slf4j与Logback组合使用,Commons Logging与Log4j组合使用。

基于下面的一些优点,选用Slf4j+Logback的日志框架:

  • 更快的执行速度,Logback重写了内部的实现,在一些关键执行路径上性能提升10倍以上。而且logback不仅性能提升了,初始化内存加载也更小了
  • 自动清除旧的日志归档文件,通过设置TimeBasedRollingPolicy 或者 SizeAndTimeBasedFNATP的 maxHistory 属性,你就可以控制日志归档文件的最大数量
  • Logback拥有远比log4j更丰富的过滤能力,可以不用降低日志级别而记录低级别中的日志。
  • Logback必须配合Slf4j使用。由于Logback和Slf4j是同一个作者,其兼容性不言而喻。
  • 默认情况下,Spring Boot会用Logback来记录日志,并用INFO级别输出到控制台。

2、配置日志

可以看到,只要集成了spring-boot-starter-web,就引入了spring-boot-starter-logging,即slf4j和logback。

其它的几个包:jcl-over-slf4j,代码直接调用common-logging会被桥接到slf4j;jul-to-slf4j,代码直接调用java.util.logging会被桥接到slf4j;log4j-over-slf4j,代码直接调用log4j会被桥接到slf4j。

还需引入janino,如果不加入这个包会报错。

在resources下添加logback.xml配置文件,Logback默认会查找classpath下的logback.xml文件。\

具体配置如下,有较详细的注释,很容易看懂。可以通过application.properties配置日志记录级别、日志输出文件目录等。

<?xml version="1.0" encoding="UTF-8"?>

<!-- 级别从高到低 OFF 、 FATAL 、 ERROR 、 WARN 、 INFO 、 DEBUG 、 TRACE 、 ALL -->
<!-- 日志输出规则 根据当前ROOT 级别,日志输出时,级别高于root默认的级别时 会输出 -->
<!-- 以下 每个配置的 filter 是过滤掉输出文件里面,会出现高级别文件,依然出现低级别的日志信息,通过filter 过滤只记录本级别的日志 -->
<!-- scan 当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。 -->
<!-- scanPeriod 设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug 当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<configuration debug="false" scan="false" scanPeriod="5 minutes">

    <!-- 引入配置文件 -->
    <property resource="application.properties"/>
    <property resource="application-${app.env:-dev}.properties"/>

    <property name="app.name" value="${app.name:-sunny}"/>
    <property name="app.env" value="${app.env:-dev}"/>

    <!-- 日志记录级别 -->
    <property name="logback_level" value="${logback.level:-DEBUG}"/>
    <!-- 是否输出日志到文件 -->
    <property name="logback_rolling" value="${logback.rolling:-false}"/>
    <!-- 设置日志输出目录 -->
    <property name="logback_rolling_path" value="${logback.rolling.path:-/data/logs}"/>
    <!-- 日志文件最大大小 -->
    <property name="logback_max_file_size" value="${logback.max_file_size:-10MB}"/>
    <!-- 格式化输出:%d:表示日期,%thread:表示线程名,%-5level:级别从左显示5个字符宽度,%logger:日志输出者的名字(通常是所在类的全名),%L:输出代码中的行号,%msg:日志消息,%n:换行符 -->
    <property name="logback_pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger %L  -| %msg%n"/>


    <if condition='p("logback_rolling").equals("true")'>
        <then>
            <!-- 滚动记录文件 -->
            <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
                <file>${logback_rolling_path}/${app.name}.log</file>
                <!-- rollingPolicy:当发生滚动时,决定RollingFileAppender的行为,涉及文件移动和重命名 -->
                <!-- TimeBasedRollingPolicy:最常用的滚动策略,它根据时间来制定滚动策略 -->
                <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                    <!-- 活动文件的名字会根据fileNamePattern的值,每隔一段时间改变一次 -->
                    <fileNamePattern>${logback_rolling_path}/${app.name}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>

                    <!-- 日志文件的保存期限为30天 -->
                    <maxHistory>30</maxHistory>

                    <timeBasedFileNamingAndTriggeringPolicy  class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                        <!-- maxFileSize:这是活动文件的大小,默认值是10MB -->
                        <maxFileSize>${logback_max_file_size}</maxFileSize>
                    </timeBasedFileNamingAndTriggeringPolicy>
                </rollingPolicy>
                <encoder>
                    <pattern>${logback_pattern}</pattern>
                    <charset>UTF-8</charset>
                </encoder>
            </appender>

            <root>
                <appender-ref ref="FILE"/>
            </root>
        </then>
    </if>


    <!-- 将日志打印到控制台 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${logback_pattern}</pattern>
        </encoder>
    </appender>

    <root level="${logback_level}">
        <appender-ref ref="CONSOLE"/>
    </root>

    <contextName>${app.name}</contextName>

</configuration>

加入配置文件后,就可以看到控制台格式化后的日志输出,还可以看到具体代码行数等,比之前的友好多了。

同时,将日志滚动输出到日志文件,保留历史记录。可通过logback.rolling=false控制是否需要输出日志到文件。

3、使用Logger

配置好之后,就可以使用Logger来输出日志了,使用起来也是非常方便。

  • 可以看到引入的包是slf4j.Logger,代码里并没有引用任何一个跟 Logback 相关的类,这便是使用 Slf4j的好处,在需要将日志框架切换为其它日志框架时,无需改动已有的代码。

  • LoggerFactory 的 getLogger() 方法接收一个参数,以这个参数决定 logger 的名字,比如第二图中的日志输出。在为 logger 命名时,用类的全限定类名作为 logger name 是最好的策略,这样能够追踪到每一条日志消息的来源

  • 可以看到,可以通过提供占位符,以参数化的方式打印日志,避免字符串拼接的不必要损耗,也无需通过logger.isDebugEnabled()这种方式判断是否需要打印。

4、全局异常处理

现在有一个问题,当日志级别设置到INFO级别后,只会输出INFO以上的日志,如INFO、WARN、ERROR,这没毛病,问题是,程序中抛出的异常堆栈(运行时异常)都没有打印了,不利于排查问题。

而且,在某些情况下,我们在Service中想直接把异常往Controller抛出不做处理,但我们不能直接把异常信息输出到客户端,这是非常不友好的。

所以,在config下建一个GlobalExceptionConfig作为全局统一异常处理。主要处理了自定义的ServiceException、AuthorityException、BaseException,以及系统的NoHandlerFoundException和Exception异常。

package com.lyyzoo.core.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;

import com.lyyzoo.core.base.Result;
import com.lyyzoo.core.constants.BaseEnums;
import com.lyyzoo.core.exception.AuthorityException;
import com.lyyzoo.core.exception.BaseException;
import com.lyyzoo.core.exception.ServiceException;
import com.lyyzoo.core.util.Results;

/**
 * 全局异常处理
 *
 * @author bojiangzhou 2018-02-06
 * @version 1.0
 */
@RestControllerAdvice
public class GlobalExceptionConfig {

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

    /**
     * 处理 ServiceException 异常
     */
    @ExceptionHandler(ServiceException.class)
    public Result handleServiceException(ServiceException e){
        Result result = Results.failure(e.getCode(), e.getMessage());
        result.setStatus(HttpStatus.BAD_REQUEST.value());
        logger.info("ServiceException[code: {}, message: {}]", e.getCode(), e.getMessage());
        return result;
    }

    /**
     * 处理 AuthorityException 异常
     */
    @ExceptionHandler(AuthorityException.class)
    public Result handleAuthorityException(AuthorityException e){
        Result result = Results.failure(BaseEnums.FORBIDDEN.code(), BaseEnums.FORBIDDEN.desc());
        result.setStatus(HttpStatus.FORBIDDEN.value());
        logger.info("AuthorityException[code: {}, message: {}]", e.getCode(), e.getMessage());
        return result;
    }

    /**
     * 处理 NoHandlerFoundException 异常. <br/>
     * 需配置 [spring.mvc.throw-exception-if-no-handler-found=true]
     * 需配置 [spring.resources.add-mappings=false]
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    public Result handleNotFoundException(NoHandlerFoundException e){
        Result result = Results.failure(BaseEnums.NOT_FOUND.code(), BaseEnums.NOT_FOUND.desc());
        result.setStatus(HttpStatus.NOT_FOUND.value());
        logger.info(e.getMessage());
        return result;
    }

    /**
     * 处理 BaseException 异常
     */
    @ExceptionHandler(BaseException.class)
    public Result handleBaseException(BaseException e){
        Result result = Results.failure(e.getCode(), e.getMessage());
        result.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        logger.error("BaseException[code: {}, message: {}]", e.getCode(), e.getMessage(), e);
        return result;
    }

    /**
     * 处理 Exception 异常
     */
    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e){
        Result result = Results.failure(BaseEnums.ERROR.code(), BaseEnums.ERROR.desc());
        result.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        logger.error(e.getMessage(), e);
        return result;
    }

}

看上面的代码,@ControllAdvice(@RestControllerAdvice可以返回ResponseBody),可看做Controller增强器,可以在@ControllerAdvice作用类下添加@ExceptionHandler,@InitBinder,@ModelAttribute注解的方法来增强Controller,都会作用在被 @RequestMapping 注解的方法上。

使用@ExceptionHandler 拦截异常,我们可以通过该注解实现自定义异常处理。在每个处理方法中,封装Result,返回对应的消息及状态码等。

通过Logger打印对应级别的日志,也可以看到控制台及日志文件中有异常堆栈的输出了。注意除了BaseException、Exception,其它的都只是打印了简单信息,且为INFO级别。Exception是ERROR级别,且打印了堆栈信息。

NoHandlerFoundException 是404异常,这里注意要先关闭DispatcherServlet的NotFound默认异常处理。

测试如下:这种返回结果就比较友好了。

    

五、数据库乐观锁

1、乐观锁

在并发修改同一条记录时,为避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存层加锁,要么在数据库层使用乐观锁,使用version作为更新依据【强制】。 —— 《阿里巴巴Java开发手册》

乐观锁,基于数据版本(version)记录机制实现,为数据库表增加一个"version"字段。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。提交数据时,提交的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

因此,这节就来处理BaseDTO中的"version"字段,通过增加一个mybatis插件来实现更新时版本号自动+1。

2、MyBatis插件介绍

MyBatis 允许在己映射语句执行过程中的某一点进行拦截调用。默认情况下, MyBatis 允许使用插件来拦截的接口和方法包括以下几个:\

  • Executor (update 、query 、flushStatements 、commit 、rollback 、getTransaction 、close 、isClosed)
  • ParameterHandler (getParameterObject 、setParameters)
  • ResultSetHandler (handleResul tSets 、handleCursorResultSets、handleOutputParameters)
  • StatementHandler (prepare 、parameterize 、batch update 、query)\

MyBatis 插件实现拦截器接口Interceptor,在实现类中对拦截对象和方法进行处理 。\

  • setProperties:传递插件的参数,可以通过参数来改变插件的行为。
  • plugin:参数 target 就是要拦截的对象,作用就是给被拦截对象生成一个代理对象,并返回。
  • intercept:会覆盖所拦截对象的原方法,Invocation参数可以反射调度原来对象的方法,可以获取到很多有用的东西。

除了需要实现拦截器接口外,还需要给实现类配置拦截器签名。 使用 @Intercepts 和 @Signature 这两个注解来配置拦截器要拦截的接口的方法,接口方法对应的签名基本都是固定的。

@Intercepts 注解的属性是一个 @Signature  数组,可以在同 一个拦截器中同时拦截不同的接口和方法。

@Signature 注解包含以下三个属性。

  • type:设置拦截的接口,可选值是前面提到的4个接口 。
  • method:设置拦截接口中的方法名, 可选值是前面4个接口对应的方法,需要和接口匹配 。
  • args:设置拦截方法的参数类型数组,通过方法名和参数类型可以确定唯一一个方法 。\

3、数据版本插件

要实现版本号自动更新,我们需要在SQL被执行前修改SQL,因此我们需要拦截的就是 StatementHandler  接口的 prepare 方法,该方法会在数据库执行前被调用,优先于当前接口的其它方法而被执行。

在 core.plugin 包下新建一个VersionPlugin插件,实现Interceptor拦截器接口。

该接口方法签名如下:

在 interceptor 方法中对 UPDATE 类型的操作,修改原SQL,加入version,修改后的SQL类似下图,更新时就会自动将version+1。同时带上version条件,如果该版本号小于数据库记录版本号,则不会更新。

VersionInterceptor插件:

package com.lyyzoo.core.plugins;

import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.operators.arithmetic.Addition;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.util.List;
import java.util.Properties;

/**
 * 乐观锁:数据版本插件
 *
 * @version 1.0
 * @author bojiangzhou 2018-02-10
 */
@Intercepts(
    @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
    )
)
public class VersionInterceptor implements Interceptor {

    private static final String VERSION_COLUMN_NAME = "version";

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

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取 StatementHandler,实际是 RoutingStatementHandler
        StatementHandler handler = (StatementHandler) processTarget(invocation.getTarget());
        // 包装原始对象,便于获取和设置属性
        MetaObject metaObject = SystemMetaObject.forObject(handler);
        // MappedStatement 是对SQL更高层次的一个封装,这个对象包含了执行SQL所需的各种配置信息
        MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        // SQL类型
        SqlCommandType sqlType = ms.getSqlCommandType();
        if(sqlType != SqlCommandType.UPDATE) {
            return invocation.proceed();
        }
        // 获取版本号
        Object originalVersion = metaObject.getValue("delegate.boundSql.parameterObject." + VERSION_COLUMN_NAME);
        if(originalVersion == null || Long.valueOf(originalVersion.toString()) <= 0){
            return invocation.proceed();
        }
        // 获取绑定的SQL
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        // 原始SQL
        String originalSql = boundSql.getSql();
        // 加入version的SQL
        originalSql = addVersionToSql(originalSql, originalVersion);
        // 修改 BoundSql
        metaObject.setValue("delegate.boundSql.sql", originalSql);

        // proceed() 可以执行被拦截对象真正的方法,该方法实际上执行了method.invoke(target, args)方法
        return invocation.proceed();
    }

    /**
     * Plugin.wrap 方法会自动判断拦截器的签名和被拦截对象的接口是否匹配,只有匹配的情况下才会使用动态代理拦截目标对象.
     *
     * @param target 被拦截的对象
     * @return 代理对象
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * 设置参数
     */
    @Override
    public void setProperties(Properties properties) {

    }

    /**
     * 获取代理的原始对象
     *
     * @param target
     * @return
     */
    private static Object processTarget(Object target) {
        if(Proxy.isProxyClass(target.getClass())) {
            MetaObject mo = SystemMetaObject.forObject(target);
            return processTarget(mo.getValue("h.target"));
        }
        return target;
    }

    /**
     * 为原SQL添加version
     *
     * @param originalSql 原SQL
     * @param originalVersion 原版本号
     * @return 加入version的SQL
     */
    private String addVersionToSql(String originalSql, Object originalVersion){
        try{
            Statement stmt = CCJSqlParserUtil.parse(originalSql);
            if(!(stmt instanceof Update)){
                return originalSql;
            }
            Update update = (Update)stmt;
            if(!contains(update)){
                buildVersionExpression(update);
            }
            Expression where = update.getWhere();
            if(where != null){
                AndExpression and = new AndExpression(where, buildVersionEquals(originalVersion));
                update.setWhere(and);
            }else{
                update.setWhere(buildVersionEquals(originalVersion));
            }
            return stmt.toString();
        }catch(Exception e){
            logger.error(e.getMessage(), e);
            return originalSql;
        }
    }

    private boolean contains(Update update){
        List<Column> columns = update.getColumns();
        for(Column column : columns){
            if(column.getColumnName().equalsIgnoreCase(VERSION_COLUMN_NAME)){
                return true;
            }
        }
        return false;
    }

    private void buildVersionExpression(Update update){
        // 列 version
        Column versionColumn = new Column();
        versionColumn.setColumnName(VERSION_COLUMN_NAME);
        update.getColumns().add(versionColumn);

        // 值 version+1
        Addition add = new Addition();
        add.setLeftExpression(versionColumn);
        add.setRightExpression(new LongValue(1));
        update.getExpressions().add(add);
    }

    private Expression buildVersionEquals(Object originalVersion){
        Column column = new Column();
        column.setColumnName(VERSION_COLUMN_NAME);

        // 条件 version = originalVersion
        EqualsTo equal = new EqualsTo();
        equal.setLeftExpression(column);
        equal.setRightExpression(new LongValue(originalVersion.toString()));
        return equal;
    }

}

之后还需配置该插件,只需要在MyBatisConfig中加入该配置即可。

最后,如果版本不匹配,更新失败,需要往外抛出异常提醒,所以修改BaseService的update方法,增加检查更新是否失败。

最后,能不用插件尽量不要用插件,因为它将修改MyBatis的底层设计。插件生成的是层层代理对象的责任链模式,通过反射方法运行,会有一定的性能消耗。

我们也可以修改 tk.mapper 生成SQL的方法,加入version,这里通过插件方式实现乐观锁主要是不为了去修改 mapper 的底层源码,比较方便。

六、Druid数据库连接池

创建数据库连接是一个很耗时的操作,也很容易对数据库造成安全隐患。对数据库连接的管理能显著影响到整个应用程序的伸缩性和健壮性,影响程序的性能指标。

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

参考:

  Druid常见问题集锦

  常用数据库连接池 (DBCP、c3p0、Druid) 配置说明

1、Druid

Druid首先是一个数据库连接池,但它不仅仅是一个数据库连接池,它还包含一个ProxyDriver,一系列内置的JDBC组件库,一个SQLParser。Druid支持所有JDBC兼容的数据库,包括Oracle、MySql、Derby、Postgresql、SQLServer、H2等等。 Druid针对Oracle和MySql做了特别优化,比如Oracle的PSCache内存占用优化,MySql的ping检测优化。Druid在监控、可扩展性、稳定性和性能方面都有明显的优势。Druid提供了Filter-Chain模式的扩展API,可以自己编写Filter拦截JDBC中的任何方法,可以在上面做任何事情,比如说性能监控、SQL审计、用户名密码加密、日志等等。

2、配置

Druid配置到core模块下,只需在application.properties中添加如下配置即可,大部分配置是默认配置,可更改。有详细的注释,比较容易理解。

####################################
# Druid
####################################
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

# 初始化连接大小[0]
spring.datasource.druid.initial-size=1
# 最小空闲连接数[0]
spring.datasource.druid.min-idle=1
# 最大连接数[8]
spring.datasource.druid.max-active=20

# 配置获取连接等待超时的时间(毫秒)[-1]
spring.datasource.druid.max-wait=60000
# 查询超时时间(秒)
spring.datasource.druid.query-timeout=90

# 用来检测连接是否有效的sql,要求是一个查询语句
spring.datasource.druid.validation-query=SELECT 'x'
# 申请连接时检测连接可用性[false]
spring.datasource.druid.test-on-borrow=false
# 归还连接检测[false]
spring.datasource.druid.test-on-return=false
# 超时是否检测连接可用性[true]
spring.datasource.druid.test-while-idle=true

# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接 (毫秒)
spring.datasource.druid.time-between-eviction-runs-millis=60000
#  配置一个连接在池中最小生存的时间(毫秒,默认30分钟)
spring.datasource.druid.min-evictable-idle-time-millis=300000
# 通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat;日志用的filter:log4j;防御sql注入的filter:wall
spring.datasource.druid.filters=stat,wall,slf4j
# 合并多个DruidDataSource的监控数据
spring.datasource.druid.use-global-data-source-stat=true

# 是否缓存PreparedStatement. PSCache对支持游标的数据库性能提升巨大,比如说oracle.在mysql下建议关闭.
spring.datasource.druid.pool-prepared-statements=false
# 每个连接上PSCache的大小
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20

# StatViewServlet [https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatViewServlet%E9%85%8D%E7%BD%AE]
spring.datasource.druid.stat-view-servlet.enabled=true
spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
# 监控页面的用户名和密码
spring.datasource.druid.stat-view-servlet.login-username=admin
spring.datasource.druid.stat-view-servlet.login-password=admin
spring.datasource.druid.stat-view-servlet.reset-enable=false

# StatFilter [https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatFilter]
spring.datasource.druid.filter.stat.db-type=mysql
#慢SQL记录
spring.datasource.druid.filter.stat.log-slow-sql=true
spring.datasource.druid.filter.stat.slow-sql-millis=2000
# SQL合并
spring.datasource.druid.filter.stat.merge-sql=false

# WallFilter [https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE-wallfilter]
spring.datasource.druid.filter.wall.enabled=true
spring.datasource.druid.filter.wall.db-type=mysql
spring.datasource.druid.filter.wall.config.delete-allow=false
spring.datasource.druid.filter.wall.config.drop-table-allow=false

之后启动项目在地址栏输入/druid/index.html并登录就可以看到Druid监控页面: