引文

  本文主要介绍如何使用mybatis插件实现拦截数据库操作并根据不同需求进行数据对比分析,主要适用于系统中需要对数据操作进行记录、在更新数据时准确记录更新字段

核心:mybatis插件(拦截器)、mybatis-Plus实体规范、数据对比

1、相关技术简介

mybatis插件

  mybatis插件实际上就是官方针对4层数据操作处理预留的拦截器,使用者可以根据不同的需求进行操作拦截并处理。这边笔者不做详细描述,详细介绍请到官网了解,这里笔者就复用官网介绍。

插件(plugins)

MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为如果在试图修改或重写已有方法的行为的时候,你很可能在破坏 MyBatis 的核心模块。 这些都是更低层的类和方法,所以使用插件的时候要特别当心。

通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。

 // ExamplePlugin.java
@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
private Properties properties = new Properties();
public Object intercept(Invocation invocation) throws Throwable {
// implement pre processing if need
Object returnObject = invocation.proceed();
// implement post processing if need
return returnObject;
}
public void setProperties(Properties properties) {
this.properties = properties;
}
}
<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>

上面的插件将会拦截在 Executor 实例中所有的 “update” 方法调用, 这里的 Executor 是负责执行低层映射语句的内部对象。

提示 覆盖配置类

除了用插件来修改 MyBatis 核心行为之外,还可以通过完全覆盖配置类来达到目的。只需继承后覆盖其中的每个方法,再把它传递到 SqlSessionFactoryBuilder.build(myConfig) 方法即可。再次重申,这可能会严重影响 MyBatis 的行为,务请慎之又慎。

重点讲下4层处理,MyBatis两级缓存就是在其中两层中实现

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

    • 所有数据库操作到达底层后都由该执行器进行任务分发,主要有update(插入、更新、删除),query(查询),提交,回滚,关闭链接等
  • ParameterHandler (getParameterObject, setParameters) 
    • 参数处理器(获取参数,设置参数)
  • ResultSetHandler (handleResultSets, handleOutputParameters) 
    • 结果集处理器(结果集,输出参数)
  • StatementHandler (prepare, parameterize, batch, update, query) 
    • 声明处理器、准备链接jdbc前处理,prepare(预处理):生成sql语句,准备链接数据库进行操作

以上4层执行顺序为顺序执行

  • Executor是 Mybatis的内部执行器,它负责调用StatementHandler操作数据库,并把结果集通过 ResultSetHandler进行自动映射,另外,他还处理了二级缓存的操作。从这里可以看出,我们也是可以通过插件来实现自定义的二级缓存的。
  • ParameterHandler是Mybatis实现Sql入参设置的对象。插件可以改变我们Sql的参数默认设置。
  • ResultSetHandler是Mybatis把ResultSet集合映射成POJO的接口对象。我们可以定义插件对Mybatis的结果集自动映射进行修改。
  • StatementHandler是Mybatis直接和数据库执行sql脚本的对象。另外它也实现了Mybatis的一级缓存。这里,我们可以使用插件来实现对一级缓存的操作(禁用等等)。

MyBatis-Plus:

  MyBatis增强器,主要规范了数据实体,在底层实现了简单的增删查改,使用者不再需要开发基础操作接口,小编认为是最强大、最方便易用的,没有之一,不接受任何反驳。详细介绍请看官网

数据实体的规范让底层操作更加便捷,本例主要实体规范中的表名以及主键获取,下面上实体规范demo

 @Data
@TableName("tb_demo")
@EqualsAndHashCode(callSuper = true)
public class Demo extends Model<Demo> {
private static final long serialVersionUID = 1L; /**
*
*/
@TableId
private Integer id;
/**
* 名称
*/
private String name; }
 

2、实现

本文所要讲述的就是在第一级(Executor)进行拦截并实现数据对比记录。
本例为公共模块实现,然后在其它模块中依赖此公共模块,根据每个模块不同的需求自定义实现不同的处理。
结构目录

一、实现拦截器

DataUpdateInterceptor,根据官网demo实现拦截器,在拦截器中根据增、删、改操作去调用各个模块中自定义实现的处理方法来达到不同的操作处理。

 package com.erp4cloud.rerp.common.data.log;

 import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.AllArgsConstructor;
import org.apache.commons.lang.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.springframework.scheduling.annotation.Async; import javax.sql.DataSource;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Properties; /**
* 数据更新拦截器
*
* @author Tophua
* @date 2019/8/2
*/
@AllArgsConstructor
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class DataUpdateInterceptor implements Interceptor { private final DataSource dataSource;
private final DataLogHandler dataLogHandler; @Override
public Object intercept(Invocation invocation) {
Object result = null;
try {
this.dealData(invocation);
result = invocation.proceed();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return result;
} @Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
}
return target;
} @Override
public void setProperties(Properties properties) { } /**
* 对数据库操作传入参数进行处理
*
* @param invocation
* @return void
* @author Tophua
* @date 2019/8/3
*/
public void dealData(Invocation invocation) {
Object[] args = invocation.getArgs();
MappedStatement mappedStatement = (MappedStatement) args[0];
// 参数
Object et = args[1];
if (et instanceof Model) {
this.doLog(mappedStatement, et);
} else if (et instanceof Map) {
String key = "et";
String listKey = "collection";
if (((Map) et).containsKey(key) && ((Map) et).get(key) instanceof Model) {
this.doLog(mappedStatement, ((Map) et).get(key));
} else if (((Map) et).containsKey(listKey) && ((Map) et).get(listKey) instanceof Collection) {
List<Object> list = (List<Object>) ((Map) et).get(listKey);
for (Object obj : list) {
if (obj instanceof Model) {
this.doLog(mappedStatement, obj);
}
}
}
}
} /**
* 根据不同参数及操作进行不同的日志记录
*
* @param mappedStatement
* @param et
* @return void
* @author Tophua
* @date 2019/8/3
*/
public void doLog(MappedStatement mappedStatement, Object et) {
// 反射获取实体类
Class<?> clazz = et.getClass();
// 不含有表名的实体就默认通过
if (!clazz.isAnnotationPresent(TableName.class)) {
return;
}
// 获取表名
TableName tableName = clazz.getAnnotation(TableName.class);
String tbName = tableName.value();
if (StringUtils.isBlank(tbName)) {
return;
}
String pkName = null;
String pkValue = null;
// 获取实体所有字段
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
// 设置些属性是可以访问的
field.setAccessible(true);
if (field.isAnnotationPresent(TableId.class)) {
// 获取主键
pkName = field.getName();
try {
// 获取主键值
pkValue = field.get(et).toString();
} catch (Exception e) {
pkValue = null;
} }
}
BasicInfo basicInfo = new BasicInfo(dataSource, (Model) et, tbName, pkName, pkValue); // 插入
if (SqlCommandType.INSERT.equals(mappedStatement.getSqlCommandType())) {
InsertInfo insertInfo = new InsertInfo(basicInfo, et);
dataLogHandler.insertHandler(insertInfo);
}
// 更新
if (SqlCommandType.UPDATE.equals(mappedStatement.getSqlCommandType()) && StringUtils.isNotBlank(pkName) && StringUtils.isNotBlank(pkValue)) {
Object oldObj = this.queryData(pkName, pkValue, (Model) et);
if (oldObj != null) {
UpdateInfo updateInfo = new UpdateInfo(basicInfo, oldObj, et);
// 调用自定义处理方法
dataLogHandler.updateHandler(updateInfo);
}
}
// 删除
if (SqlCommandType.DELETE.equals(mappedStatement.getSqlCommandType()) && StringUtils.isNotBlank(pkName) && StringUtils.isNotBlank(pkValue)) {
Object delObj = this.queryData(pkName, pkValue, (Model) et);
if (delObj != null) {
DeleteInfo deleteInfo = new DeleteInfo(basicInfo, delObj);
// 调用自定义处理方法
dataLogHandler.deleteHandler(deleteInfo);
}
}
} /**
* 根据主键和主键值查询数据
*
* @param pkName
* @param pkValue
* @param clazz
* @return java.lang.Object
* @author Tophua
* @date 2019/8/5
*/
private Object queryData(String pkName, String pkValue, Model clazz) {
// 查询更新前数据
return clazz.selectOne(Wrappers.query().eq(pkName, pkValue));
}
}

二、配置

 package com.erp4cloud.rerp.common.data.log;

 import lombok.AllArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; /**
* 数据更新日志处理配置(实现按需加载)
*
* @author Tophua
* @date 2019/8/2
*/
@Configuration
@AllArgsConstructor
@ConditionalOnBean({DataSource.class, DataLogHandler.class})
public class DataLogConfig { private final DataLogHandler dataLogHandler;
private final DataSource dataSource; @Bean
@ConditionalOnMissingBean
public DataUpdateInterceptor dataUpdateInterceptor() {
return new DataUpdateInterceptor(dataSource, dataLogHandler);
}
}

提示:公共模块中需要在spring.factories(src/main/resources/META-INF/)中进行配置让Spring自动进行装配,小笔使用如下

 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.erp4cloud.rerp.common.data.log.DataLogConfig

三、在其它模块实现自定义处理接口

在接口中会根据不同操作传入不同参数,各位可以根据具体方法取出数据进行所需操作。

本例仅测试使用,未实现具体操作,在具体使用中请自行编写具体逻辑。

DataLogHandler 自定义处理接口,各模块实现,注意需要将实现类作为ServiceBean使用,否则该功能无法生效。

 package com.erp4cloud.rerp.common.data.log;

 /**
* 数据日志处理
*
* @author Tophua
* @date 2019/8/2
*/
public interface DataLogHandler { /**
* 插入处理
*
* @param insertInfo 插入数据信息
* @return void
* @author Tophua
* @date 2019/8/2
*/
void insertHandler(InsertInfo insertInfo); /**
* 更新处理
*
* @param updateInfo 更新数据信息
* @return void
* @author Tophua
* @date 2019/8/2
*/
void updateHandler(UpdateInfo updateInfo); /**
* 删除处理
*
* @param deleteInfo 删除数据信息
* @return void
* @author Tophua
* @date 2019/8/3
*/
void deleteHandler(DeleteInfo deleteInfo);
}

实现demo

 package com.erp4cloud.rerp.building.log;

 import com.erp4cloud.rerp.common.data.log.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.stereotype.Service; import java.util.List; /**
* describe
*
* @author Tophua
* @date 2019/8/3
*/
@Service
@AllArgsConstructor
public class DataLogDeal implements DataLogHandler { ObjectMapper objectMapper = new ObjectMapper(); @SneakyThrows
@Override
public void insertHandler(InsertInfo insertInfo) {
System.out.println("插入:" + objectMapper.writeValueAsString(insertInfo.getInsertObj()));
} @SneakyThrows
@Override
public void updateHandler(UpdateInfo updateInfo) {
List<CompareResult> cr = updateInfo.getCompareResult();
StringBuilder sb = new StringBuilder();
sb.append("更新\"");
sb.append(updateInfo.getBasicInfo().getTbName());
sb.append("\" 表 ");
cr.forEach(r -> {
String s = "把《" + r.getFieldComment() + "》从<" + r.getOldValue() + ">改成<" + r.getNewValue() + ">";
sb.append(s);
});
System.out.println(sb.toString());
} @SneakyThrows
@Override
public void deleteHandler(DeleteInfo deleteInfo) {
System.out.println("删除:" + objectMapper.writeValueAsString(deleteInfo.getDeleteObj()));
}
}

四、其它代码

BaseDataLogHandler 基础处理抽象类,提供底层数据对比方法。

 package com.erp4cloud.rerp.common.data.log;

 import lombok.AllArgsConstructor;
import lombok.Getter; import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional; /**
* 数据日志基础信息及处理
*
* @author Tophua
* @date 2019/8/5
*/
@Getter
@AllArgsConstructor
public abstract class BaseDataLogHandler { /**
* 数据基础信息
*/
private BasicInfo basicInfo; /**
* 对比两个对象
*
* @param oldObj 旧对象
* @param newObj 新对象
* @return java.util.List<com.erp4cloud.rerp.common.data.log.CompareResult>
* @author Tophua
* @date 2019/8/5
*/
protected List<CompareResult> compareTowObject(Object oldObj, Object newObj) throws IllegalAccessException {
List<CompareResult> list = new ArrayList<>();
//获取对象的class
Class<?> clazz1 = oldObj.getClass();
Class<?> clazz2 = newObj.getClass();
//获取对象的属性列表
Field[] field1 = clazz1.getDeclaredFields();
Field[] field2 = clazz2.getDeclaredFields();
//遍历属性列表field1
for (int i = 0; i < field1.length; i++) {
//遍历属性列表field2
for (int j = 0; j < field2.length; j++) {
//如果field1[i]属性名与field2[j]属性名内容相同
if (field1[i].getName().equals(field2[j].getName())) {
field1[i].setAccessible(true);
field2[j].setAccessible(true);
if (field2[j].get(newObj) == null) {
continue;
}
//如果field1[i]属性值与field2[j]属性值内容不相同
if (!compareTwo(field1[i].get(oldObj), field2[j].get(newObj))) {
CompareResult r = new CompareResult();
r.setFieldName(field1[i].getName());
r.setOldValue(field1[i].get(oldObj));
r.setNewValue(field2[j].get(newObj)); // 匹配字段注释
Optional o = this.basicInfo.getFieldInfos().stream()
.filter(f -> r.getFieldName().equals(f.getJFieldName())).findFirst();
if (o.isPresent()) {
r.setFieldComment(((FieldInfo) o.get()).getComment());
}
list.add(r);
}
break;
}
}
}
return list;
} /**
* 对比两个数据是否内容相同
*
* @param object1,object2
* @return boolean类型
*/
private boolean compareTwo(Object object1, Object object2) { if (object1 == null && object2 == null) {
return true;
}
if (object1 == null && object2 != null) {
return false;
}
if (object1.equals(object2)) {
return true;
}
return false;
} }

BasicInfo 基础信息,数据源,本表字段信息等。

 package com.erp4cloud.rerp.common.data.log;

 import cn.hutool.db.Db;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Getter;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils; import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap; /**
* 基础信息
*
* @author Tophua
* @date 2019/8/5
*/
@Getter
public class BasicInfo {
private static ConcurrentHashMap<String, List<FieldInfo>> fields = new ConcurrentHashMap<>(); /**
* 数据源
*/
private DataSource dataSource;
/**
* mybatis数据底层
*/
private Model model;
/**
* 表名
*/
private String tbName;
/**
* 主键名称
*/
private String pkName;
/**
* 主键值
*/
private String pkValue; /**
* 表字段注释
*/
private List<FieldInfo> fieldInfos; public BasicInfo(DataSource dataSource, Model model, String tbName, String pkName, String pkValue) {
this.dataSource = dataSource;
this.model = model;
this.tbName = tbName;
this.pkName = pkName;
this.pkValue = pkValue;
} public List<FieldInfo> getFieldInfos() {
if (!fields.containsKey(this.tbName)) {
String query = "select column_name fieldName, column_comment comment from information_schema.columns" +
" where table_name = \"" + this.tbName + "\" and table_schema = (select database())";
try {
this.fieldInfos = Db.use(dataSource).query(query, FieldInfo.class);
} catch (SQLException e) {
this.fieldInfos = new ArrayList<>();
}
this.fieldInfos.forEach(f -> {
String caseName = this.columnToJava(f.getFieldName());
f.setJFieldName(StringUtils.uncapitalize(caseName));
});
fields.put(this.tbName, this.fieldInfos);
}
return fields.get(this.tbName);
} /**
* 列名转换成Java属性名
*/
private String columnToJava(String columnName) {
return WordUtils.capitalizeFully(columnName, new char[]{'_'}).replace("_", "");
}
}

FieldInfo 字段信息

 package com.erp4cloud.rerp.common.data.log;

 import lombok.Data;

 /**
* 字段信息
*
* @author Tophua
* @date 2019/8/5
*/
@Data
public class FieldInfo { /**
* 字段名
*/
private String fieldName;
/**
* java字段名
*/
private String jFieldName;
/**
* 注释
*/
private String comment;
}

CompareResult 字段对比结果

 package com.erp4cloud.rerp.common.data.log;

 import lombok.Data;

 /**
* 对比两个对象结果
*
* @author Tophua
* @date 2019/8/5
*/
@Data
public class CompareResult { /**
* 字段名
*/
private String fieldName;
/**
* 字段注释
*/
private String fieldComment;
/**
* 字段旧值
*/
private Object oldValue;
/**
* 字段新值
*/
private Object newValue;
}

InsertInfo 插入信息

 package com.erp4cloud.rerp.common.data.log;

 import lombok.Getter;

 /**
* 数据插入信息
*
* @author Tophua
* @date 2019/8/5
*/
@Getter
public class InsertInfo extends BaseDataLogHandler { /**
* 插入对象
*/
private Object insertObj; public InsertInfo(BasicInfo basicInfo, Object insertObj) {
super(basicInfo);
this.insertObj = insertObj;
} }

UpdateInfo 更新信息

 package com.erp4cloud.rerp.common.data.log;

 import lombok.Getter;

 import java.util.List;

 /**
* 数据更新信息
*
* @author Tophua
* @date 2019/8/5
*/
@Getter
public class UpdateInfo extends BaseDataLogHandler { /**
* 更新前对象
*/
private Object oldObj;
/**
* 更新对象
*/
private Object newObj; public UpdateInfo(BasicInfo basicInfo, Object oldObj, Object newObj) {
super(basicInfo);
this.oldObj = oldObj;
this.newObj = newObj;
} public List<CompareResult> getCompareResult() throws IllegalAccessException {
return compareTowObject(this.oldObj, this.newObj);
}
}

DeleteInfo 删除信息

package com.erp4cloud.rerp.common.data.log;

import lombok.Getter;

/**
* 数据删除信息
*
* @author Tophua
* @date 2019/8/5
*/
@Getter
public class DeleteInfo extends BaseDataLogHandler { /**
* 删除对象
*/
private Object deleteObj; public DeleteInfo(BasicInfo basicInfo, Object deleteObj) {
super(basicInfo);
this.deleteObj = deleteObj;
}
}

四、结果

更新时控制台打印:

 更新"customer_resource_base_info" 表 把《姓名》从<测试>改成<测试dsffgggg>把《身份证》从<2222222>改成<3333333333333> 

由于是测试所以未进行数据库保存,大家自行保存。

3、总结

本例主要解决多实体数据更新前后对比记录,当然也可使用AOP实现数据对比,但经笔者实现感觉还是此方法实现起来相对简单。笔者更推荐使用底层技术直接进行拦截处理,这样能保证任何数据操作都毫无遗漏,不放过任何操作。

目前本例暂未实现数据无主键更新记录,但业务中经常会出现无主键根据其它条件更新,所以本例还可进行优化提升,在此笔者就先放一段了,等后续再进行升级更新。

欢迎各位大神交流意见。。。。。。

SpringCloud或SpringBoot+Mybatis-Plus利用mybatis插件实现数据操作记录及更新对比的更多相关文章

  1. SpringCloud或SpringBoot+Mybatis-Plus利用AOP+mybatis插件实现数据操作记录及更新对比

    引文 本文主要介绍如何使用Spring AOP + mybatis插件实现拦截数据库操作并根据不同需求进行数据对比分析,主要适用于系统中需要对数据操作进行记录.在更新数据时准确记录更新字段 核心:AO ...

  2. Mysql之binlog日志说明及利用binlog日志恢复数据操作记录

    众所周知,binlog日志对于mysql数据库来说是十分重要的.在数据丢失的紧急情况下,我们往往会想到用binlog日志功能进行数据恢复(定时全备份+binlog日志恢复增量数据部分),化险为夷! 一 ...

  3. 【转】Mysql之binlog日志说明及利用binlog日志恢复数据操作记录

    众所周知,binlog日志对于mysql数据库来说是十分重要的.在数据丢失的紧急情况下,我们往往会想到用binlog日志功能进行数据恢复(定时全备份+binlog日志恢复增量数据部分),化险为夷! 废 ...

  4. Mysql利用binlog日志恢复数据操作(转)

    a.开启binlog日志:1)编辑打开mysql配置文件/etc/mys.cnf[root@vm-002 ~]# vim /etc/my.cnf在[mysqld] 区块添加 log-bin=mysql ...

  5. Mybatis分页-利用Mybatis Generator插件生成基于数据库方言的分页语句,统计记录总数 (转)

    众所周知,Mybatis本身没有提供基于数据库方言的分页功能,而是基于JDBC的游标分页,很容易出现性能问题.网上有很多分页的解决方案,不外乎是基于Mybatis本机的插件机制,通过拦截Sql做分页. ...

  6. springboot+mybatis+mysql 利用mybatis自动生成sql语句

    工具和环境 idea,mysql,JDK1.8 效果图如下 结构图如下 java resources sql文件 /* Navicat MySQL Data Transfer Source Serve ...

  7. SpringBoot入门项目CRM学习过程中的报错记录(更新ing)

    在用mybatis自动生成实体类和mapper时报错..... is unrecognized or represents more than one time zone. You must conf ...

  8. Springboot 系列(十一)使用 Mybatis(自动生成插件) 访问数据库

    1. Springboot mybatis 介绍 MyBatis 是一款优秀的持久层框架,它支持定制化 SQL.存储过程以及高级映射.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数获取 ...

  9. Springboot 系列(十二)使用 Mybatis 集成 pagehelper 分页插件和 mapper 插件

    前言 在 Springboot 系列文章第十一篇里(使用 Mybatis(自动生成插件) 访问数据库),实验了 Springboot 结合 Mybatis 以及 Mybatis-generator 生 ...

随机推荐

  1. WPF学习笔记二之依赖属性

    1.快捷生成依赖属性:propdp然后按两次tab键 2.应用场景:自定义控件 什么是依赖属性:依赖属性自己没有值,通过依赖别人(如Binding)来获得值. 依赖属性为什么会出现:控件常用字段有限, ...

  2. U盘拷贝目标文件过大无法复制时的解决方法

    在cmd下输入:convert U盘符:/fs:ntfs    --->  回车 转换完成后可以看到U盘属性为NTFS了 这时就可以复制大文件到U盘了

  3. Docker+JMeter单机版+File Browser

    基于JMeter5.1.1+File Browser2.1.0  JMeter发起压测  File Browser作为文件服务器 一.目录结构: Dockerfile文件: FROM ubuntu:1 ...

  4. DOJ1187 : 重建家园 (分数规划 && 二分 && kruskal)

    最优答案一定是一颗树 那么二分比值,不断kruskal找到最大可以满足的解就可以了 代码如下 #include <cstdio> #include <algorithm> us ...

  5. HTML学习(5)标题、水平线、注释

    HTML 标题 标题(Heading)是通过 <h1> - <h6> 标签进行定义的. <h1> 定义最大的标题. <h6> 定义最小的标题. 注: 浏 ...

  6. springMVC 校验时,CustomValidationMessages.properties中的错误提示信息的中文乱码 问题

    今天在学习springmvc的校验时,遇到了CustomValidationMessages.properties配置文件的信息,才错误提示时是乱码的问题:在网上找了很多方法都没解决:最后原来是在配置 ...

  7. Saber-图集

    PS:狙击手

  8. Bugku-CTF之这是一个神奇的登陆框

    Day32 这是一个神奇的登陆框 http://123.206.87.240:9001/sql/ flag格式flag{}  

  9. 【转】html5中如何去掉input type date默认样式

    html5中如何去掉input type date默认样式1.时间选择的种类:HTML代码: 选择日期:<input type="date" value="2017 ...

  10. [Leetcode] 攻略计划

    在同龄人中我的逻辑思维应该是中下水平,要好好练练.而训练的内容之一,就是解决信息问题,锻炼建模能力. 今天先从队列和栈开始,这两种数据结构大概是计算机和许多程序中最重要的组成部分了.