Java框架搭建实用开发
一、全局异常处理
1. 创建异常捕获类
import com.qj.bean.Result;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;/*** 全局异常处理类* 使用@RestControllerAdvice注解标注,用于统一处理控制器层抛出的异常* 避免异常信息直接暴露给客户端,提供统一的异常响应格式*/
@RestControllerAdvice
public class ExceptionAdaptController {/*** 运行时异常处理方法* 捕获所有RuntimeException及其子类异常** @param exception 运行时异常对象* @return 统一的错误结果封装*/@ExceptionHandler({RuntimeException.class})public Result runtimeException(RuntimeException exception) {// 打印异常堆栈信息到控制台,便于开发调试exception.printStackTrace();// 将异常信息封装到统一结果对象中返回给前端return Result.error(exception.getMessage());}/*** 参数校验异常处理方法* 专门处理Spring Validation参数校验失败抛出的BindException异常** @param exception 参数绑定异常对象* @return 统一的错误结果封装,只返回第一个错误信息*/@ExceptionHandler({BindException.class})public Result validExceptionHandler(BindException exception) {// 获取所有校验错误信息,取第一个错误信息返回给前端return Result.error(exception.getAllErrors().get(0).getDefaultMessage());}/*** 通用异常处理方法* 捕获所有未被上述方法处理的Exception及其子类异常** @param exception 异常对象* @return 统一的错误结果封装*/@ExceptionHandler({Exception.class})public Result exception(Exception exception) {// 打印异常堆栈信息到控制台,便于开发调试exception.printStackTrace();// 将异常信息封装到统一结果对象中返回给前端return Result.error(exception.getMessage());}
}
二、数据实体公共属性抽取
说明:实体类中,有红框内的共有字段,可是业务中不止这一个实体类有这些字段,可能所有的实体类都会有这些字段。
问题:我们可不可以将这些共有字段抽取出来,整个Po不再关注这些共有字段?
1. 抽取共有字段实体类BaseEntity
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;import java.io.Serializable;
import java.util.Date;@Data
public class BaseEntity implements Serializable {@TableField(fill = FieldFill.INSERT)private String createBy;@TableField(fill = FieldFill.INSERT)private Date createTime;@TableField(fill = FieldFill.UPDATE)private String updateBy;@TableField(fill = FieldFill.UPDATE)private Date updateTime;@TableField(fill = FieldFill.INSERT)private Integer deleteFlag;@TableField(fill = FieldFill.INSERT)private Integer version;
}
2. 实体类继承BaseEntity
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.qj.entity.BaseEntity;
import lombok.Data;@TableName(value = "user")
@Data
public class UserPo extends BaseEntity {@TableId(value = "id", type = IdType.AUTO)private Long id;private String name;private Integer age;
}
三、mybatis属性自动填充
给数据库新增数据时,createBy、updateBy、createTime、updateTime、deleteFlag、version等字段手动进行set操作那么效率极低,而MybatisPlus为我们添加了相关拦截器操作!
说明:MetaObjectHandler接口是mybatisPlus为我们提供的的一个扩展接口,我们可以利用这个接口在我们插入或者更新数据的时候,为一些字段指定默认值(实现这个需求的方法不止一种,在SQL层面也可以做到,在建表的时候也可以指定默认值,但都没有如此灵活)
1. 创建自定义MetaObjectHandler类
注意:因为@ComponentScan(value = "com.xxx")注解的关系,所以要将我们的自定义的Handler给放入com.xxx包下!
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;import java.util.Date;@Component
public class CustomMetaObjectHandler implements MetaObjectHandler {@Overridepublic void insertFill(MetaObject metaObject) {this.strictInsertFill(metaObject, "createBy", String.class, "qj");this.strictInsertFill(metaObject, "createTime", Date.class, new Date());this.strictInsertFill(metaObject, "deleteFlag", Integer.class, 0);this.strictInsertFill(metaObject, "version", Integer.class, 0);}@Overridepublic void updateFill(MetaObject metaObject) {this.strictUpdateFill(metaObject, "updateBy", String.class, "qj");this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());}
}
2. 实体类属性修改
参数说明:实体类的属性上加入 @TableField 注解
- @TableField(fill = FieldFill.INSERT) :插入操作的时候生效
- @TableField(fill = FieldFill.UPDATE) :更新操作的时候生效
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;import java.util.Date;@TableName(value = "user")
@Data
public class UserPo {@TableId(value = "id", type = IdType.AUTO)private Long id;private String name;private Integer age;@TableField(fill = FieldFill.INSERT)private String createBy;@TableField(fill = FieldFill.INSERT)private Date createTime;@TableField(fill = FieldFill.UPDATE)private String updateBy;@TableField(fill = FieldFill.UPDATE)private Date updateTime;@TableField(fill = FieldFill.INSERT)private Integer deleteFlag;@TableField(fill = FieldFill.INSERT)private Integer version;
}
四、 MybatisPlus 优化器(显示完整SQL)
注意:可以看到Druid监控中是看不到具体的完整SQL的,那么如果我们想要显示完整SQL,就需要添加一个 MybatisPlus 优化器
2. 创建SqlBeautyInterceptor类
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.defaults.DefaultSqlSession.StrictMap;import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.*;/*** SQL美化拦截器* 用于拦截MyBatis的SQL执行,格式化SQL语句并记录执行时间* 通过实现Interceptor接口,使用@Intercepts注解定义拦截点*/
@Intercepts(value = {// 拦截StatementHandler的query方法@Signature(args = {Statement.class, ResultHandler.class}, method = "query", type = StatementHandler.class),// 拦截StatementHandler的update方法@Signature(args = {Statement.class}, method = "update", type = StatementHandler.class),// 拦截StatementHandler的batch方法@Signature(args = {Statement.class}, method = "batch", type = StatementHandler.class)})
public class SqlBeautyInterceptor implements Interceptor {/*** 拦截方法核心实现* 在执行SQL前后记录时间,并格式化输出SQL语句** @param invocation 方法调用信息* @return 原方法执行结果* @throws Throwable 可能抛出的异常*/@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 获取被拦截的目标对象Object target = invocation.getTarget();// 记录SQL开始执行时间long startTime = System.currentTimeMillis();// 转换为StatementHandlerStatementHandler statementHandler = (StatementHandler) target;try {// 执行原方法(即SQL执行)return invocation.proceed();} finally {// 计算SQL执行耗时long endTime = System.currentTimeMillis();long sqlCost = endTime - startTime;// 获取绑定的SQL信息BoundSql boundSql = statementHandler.getBoundSql();String sql = boundSql.getSql();Object parameterObject = boundSql.getParameterObject();List<ParameterMapping> parameterMappingList = boundSql.getParameterMappings();// 格式化SQL(将占位符替换为实际参数值)sql = formatSql(sql, parameterObject, parameterMappingList);// 输出格式化后的SQL及执行时间System.out.println("SQL: [ " + sql + " ]执行耗时[ " + sqlCost + "ms ]");}}/*** 插件封装方法* 用于将拦截器应用到目标对象** @param o 目标对象* @return 代理后的对象*/@Overridepublic Object plugin(Object o) {return Plugin.wrap(o, this);}/*** 设置属性方法* 可以从配置中获取属性值** @param properties 配置属性*/@Overridepublic void setProperties(Properties properties) {// 此处未实现属性设置逻辑}/*** 格式化SQL方法* 将SQL中的占位符?替换为实际的参数值** @param sql 原始SQL* @param parameterObject 参数对象* @param parameterMappingList 参数映射列表* @return 格式化后的SQL*/private String formatSql(String sql, Object parameterObject, List<ParameterMapping> parameterMappingList) {// 空SQL检查if (sql == "" || sql.length() == 0) {return "";}// 美化SQL(去除多余空格和换行)sql = beautifySql(sql);// 无参数时直接返回美化后的SQLif (parameterObject == null || parameterMappingList == null || parameterMappingList.size() == 0) {return sql;}// 保存一份未替换占位符的SQL,用于异常时返回String sqlWithoutReplacePlaceholder = sql;try {if (parameterMappingList != null) {Class<?> parameterObjectClass = parameterObject.getClass();// 处理MyBatis默认的StrictMap参数(通常是foreach循环产生的集合参数)if (isStrictMap(parameterObjectClass)) {StrictMap<Collection<?>> strictMap = (StrictMap<Collection<?>>) parameterObject;if (isList(strictMap.get("list").getClass())) {sql = handleListParameter(sql, strictMap.get("list"));}} // 处理Map类型参数else if (isMap(parameterObjectClass)) {Map<?, ?> paramMap = (Map<?, ?>) parameterObject;sql = handleMapParameter(sql, paramMap, parameterMappingList);} // 处理普通JavaBean参数else {sql = handleCommonParameter(sql, parameterMappingList, parameterObjectClass, parameterObject);}}} catch (Exception e) {// 发生异常时返回未替换占位符的SQLreturn sqlWithoutReplacePlaceholder;}return sql;}/*** 处理普通JavaBean参数* 通过反射获取字段值并替换SQL中的占位符** @param sql SQL语句* @param parameterMappingList 参数映射列表* @param parameterObjectClass 参数对象类* @param parameterObject 参数对象* @return 处理后的SQL* @throws Exception 反射可能抛出的异常*/private String handleCommonParameter(String sql, List<ParameterMapping> parameterMappingList,Class<?> parameterObjectClass, Object parameterObject) throws Exception {// 保存原始参数类Class<?> originalParameterObjectClass = parameterObjectClass;// 收集所有字段(包括父类字段)List<Field> allFieldList = new ArrayList<>();while (parameterObjectClass != null) {allFieldList.addAll(new ArrayList<>(Arrays.asList(parameterObjectClass.getDeclaredFields())));parameterObjectClass = parameterObjectClass.getSuperclass();}// 转换为字段数组Field[] fields = new Field[allFieldList.size()];fields = allFieldList.toArray(fields);// 恢复参数类parameterObjectClass = originalParameterObjectClass;// 遍历参数映射,替换占位符for (ParameterMapping parameterMapping : parameterMappingList) {String propertyValue = null;// 处理基本类型及其包装类if (isPrimitiveOrPrimitiveWrapper(parameterObjectClass)) {propertyValue = parameterObject.toString();} else {// 获取属性名String propertyName = parameterMapping.getProperty();Field field = null;// 查找对应的字段for (Field everyField : fields) {if (everyField.getName().equals(propertyName)) {field = everyField;}}// 设置字段可访问(针对私有字段)field.setAccessible(true);// 获取字段值propertyValue = String.valueOf(field.get(parameterObject));// 字符串类型添加引号if (parameterMapping.getJavaType().isAssignableFrom(String.class)) {propertyValue = "\"" + propertyValue + "\"";}}// 替换第一个占位符sql = sql.replaceFirst("\\?", propertyValue);}return sql;}/*** 处理Map类型参数* 从Map中获取值替换SQL中的占位符** @param sql SQL语句* @param paramMap 参数Map* @param parameterMappingList 参数映射列表* @return 处理后的SQL*/private String handleMapParameter(String sql, Map<?, ?> paramMap, List<ParameterMapping> parameterMappingList) {for (ParameterMapping parameterMapping : parameterMappingList) {Object propertyName = parameterMapping.getProperty();Object propertyValue = paramMap.get(propertyName);if (propertyValue != null) {// 字符串类型添加引号if (propertyValue.getClass().isAssignableFrom(String.class)) {propertyValue = "\"" + propertyValue + "\"";}// 替换占位符sql = sql.replaceFirst("\\?", propertyValue.toString());}}return sql;}/*** 处理List类型参数* 遍历List中的值替换SQL中的占位符** @param sql SQL语句* @param col 集合参数* @return 处理后的SQL*/private String handleListParameter(String sql, Collection<?> col) {if (col != null && col.size() != 0) {for (Object obj : col) {String value = null;Class<?> objClass = obj.getClass();// 处理基本类型及其包装类if (isPrimitiveOrPrimitiveWrapper(objClass)) {value = obj.toString();} // 字符串类型添加引号else if (objClass.isAssignableFrom(String.class)) {value = "\"" + obj.toString() + "\"";}// 替换占位符sql = sql.replaceFirst("\\?", value);}}return sql;}/*** SQL美化方法* 去除多余的空格和换行符** @param sql 原始SQL* @return 美化后的SQL*/private String beautifySql(String sql) {// 将多个连续空格和换行符替换为单个空格sql = sql.replaceAll("[\\s\n ]+", " ");return sql;}/*** 判断是否为基本类型或基本类型的包装类** @param parameterObjectClass 待检查的类* @return true-是基本类型或包装类,false-不是*/private boolean isPrimitiveOrPrimitiveWrapper(Class<?> parameterObjectClass) {return parameterObjectClass.isPrimitive() || (parameterObjectClass.isAssignableFrom(Byte.class)|| parameterObjectClass.isAssignableFrom(Short.class)|| parameterObjectClass.isAssignableFrom(Integer.class)|| parameterObjectClass.isAssignableFrom(Long.class)|| parameterObjectClass.isAssignableFrom(Double.class)|| parameterObjectClass.isAssignableFrom(Float.class)|| parameterObjectClass.isAssignableFrom(Character.class)|| parameterObjectClass.isAssignableFrom(Boolean.class));}/*** 判断是否为DefaultSqlSession的内部类StrictMap** @param parameterObjectClass 待检查的类* @return true-是StrictMap,false-不是*/private boolean isStrictMap(Class<?> parameterObjectClass) {return parameterObjectClass.isAssignableFrom(StrictMap.class);}/*** 判断是否为List的实现类** @param clazz 待检查的类* @return true-是List实现类,false-不是*/private boolean isList(Class<?> clazz) {Class<?>[] interfaceClasses = clazz.getInterfaces();for (Class<?> interfaceClass : interfaceClasses) {if (interfaceClass.isAssignableFrom(List.class)) {return true;}}return false;}/*** 判断是否为Map的实现类** @param parameterObjectClass 待检查的类* @return true-是Map实现类,false-不是*/private boolean isMap(Class<?> parameterObjectClass) {Class<?>[] interfaceClasses = parameterObjectClass.getInterfaces();for (Class<?> interfaceClass : interfaceClasses) {if (interfaceClass.isAssignableFrom(Map.class)) {return true;}}return false;}
}
2. 创建MybatisConfiguration类
说明:如果我们想要使用 SqlBeautyInterceptor类,那么我们需要将这个类集成进我们的框架,就需要创建相应的配置类!
import com.qj.intecepter.SqlBeautyInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class MybatisConfiguration {// 注入Bean容器@Beanpublic SqlBeautyInterceptor sqlBeautyInterceptor() {return new SqlBeautyInterceptor();}
}
3. 优化:SQL优化器动态生效
问题:如果业务方不想要这个打印日志,怎么进行动态生效呢?
(1)在配置类上加上@Conditional注解
说明:只需要在自动装配的Bean上加上 @ConditionalOnProperty 注解即可(@ConditionalOnBean 注解也可实现此功能)
import com.qj.intecepter.SqlBeautyInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class MybatisConfiguration {// 注入Bean容器@Bean@ConditionalOnProperty(value = {"sql.beauty.show"}, havingValue = "true", matchIfMissing = true)public SqlBeautyInterceptor sqlBeautyInterceptor() {return new SqlBeautyInterceptor();}
}
(2)配置文件中进行配置
说明:不配置或者配置为true时会打印SQL日志,但是配置为false就不再打印日志,这次是否生效交由业务方来定夺!
sql:beauty:show: true
五、逻辑删除拦截器(配置MybatisPlus逻辑删除)
问题:因为我们数据库中有逻辑删除的delete_flag字段,那么我们每次进行删除的时候,需要手动去update为1。
说明:如果可以自动化修改,那么可以节省我们大量的操作,MybatisPlus给我们提供了这一功能!
1. 添加相关配置
mybatis-plus:global-config:db-config:logic-delete-field: delete_flaglogic-delete-value: 1logic-not-delete-value: 0
注意:逻辑删除字段logic-delete-field要写数据库里的字段,而不是实体内中的!