主要技术
ORM框架-Mybatis Plus
MyBatis Plus是在 MyBatis 的基础上只做增强不做改变,可以简化开发,提高效率.
Mybatis Plus核心功能
支持通用的 CRUD,代码生成器与条件构造器
通用CRUD: 定义好Mapper接口后,只需要继承 BaseMapper
接口即可获得通用的增删改查功能,无需编写任何接口方法与配置文件
条件构造器: 通过EntityWrapper
(实体包装类),可以用于拼接SQL语句,并且支持排序,分组查询等复杂的 SQL
代码生成器: 支持一系列的策略配置与全局配置,比 MyBatis 的代码生成更好用
BaseMapper接口中通用的 CRUD 方法:
MyBatis Plus与SpringBoot集成
DROP TABLE IF EXISTS user;CREATE TABLE user( id bigint(20) DEFAULT NULL COMMENT '唯一标示',
code varchar(20) DEFAULT NULL COMMENT '编码', name varchar(64) DEFAULT NULL COMMENT '名称', status char(1) DEFAULT 1 COMMENT '状态 1启用 0 停用',
gmt_create datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
gmt_modified datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间') ENGINE=InnoDB DEFAULT CHARSET=utf8;
<!--mybatis plus --><dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatisplus-spring-boot-starter</artifactId>
<version>1.0.5</version></dependency><dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>2.1.9</version></dependency>
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--创建jdbc数据源 这里直接使用阿里的druid数据库连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="driverClassName" value="${mysql.driver}"/>
<property name="url" value="${mysql.url}"/>
<property name="username" value="${mysql.username}"/>
<property name="password" value="${mysql.password}"/>
<!-- 初始化连接大小 -->
<property name="initialSize" value="0"/>
<!-- 连接池最大使用连接数量 -->
<property name="maxActive" value="20"/>
<!-- 连接池最大空闲 -->
<property name="maxIdle" value="20"/>
<!-- 连接池最小空闲 -->
<property name="minIdle" value="0"/>
<!-- 获取连接最大等待时间 -->
<property name="maxWait" value="60000"/>
<property name="validationQuery" value="${validationQuery}"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
<property name="testWhileIdle" value="true"/>
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="25200000"/>
<!-- 打开removeAbandoned功能 -->
<property name="removeAbandoned" value="true"/>
<!-- 1800秒,也就是30分钟 -->
<property name="removeAbandonedTimeout" value="1800"/>
<!-- 关闭abanded连接时输出错误日志 -->
<property name="logAbandoned" value="true"/>
<!-- 监控数据库 -->
<property name="filters" value="mergeStat"/>
</bean>
<!-- (事务管理)transaction manager, use JtaTransactionManager for global tx -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 可通过注解控制事务 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
<!--mybatis-->
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- 自动扫描mapper.xml文件,支持通配符 -->
<property name="mapperLocations" value="classpath:mapper/**/*.xml"/>
<!-- 配置文件,比如参数配置(是否启动驼峰等)、插件配置等 -->
<property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/>
<!-- 启用别名,这样就无需写全路径类名了,具体可自行查阅资料 -->
<property name="typeAliasesPackage" value="cn.lqdev.learning.springboot.chapter9.biz.entity"/>
<!-- MP 全局配置注入 -->
<property name="globalConfig" ref="globalConfig"/>
</bean>
<bean id="globalConfig" class="com.baomidou.mybatisplus.entity.GlobalConfiguration">
<!--
AUTO->`0`("数据库ID自增")QW
INPUT->`1`(用户输入ID")
ID_WORKER->`2`("全局唯一ID")
UUID->`3`("全局唯一ID")
-->
<property name="idType" value="3" />
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 自动扫描包路径,接口自动注册为一个bean类 -->
<property name="basePackage" value="cn.lqdev.learning.springboot.chapter9.biz.dao"/>
</bean></beans>
@Configuration@ImportResource(locations = {"classpath:/mybatis/spring-mybatis.xml"})//@MapperScan("cn.lqdev.learning.springboot.chapter9.biz.dao")//@EnableTransactionManagementpublic class MybatisPlusConfig {
}
MyBatis Plus集成Spring
DROP TABLE IF EXISTS tbl_employee;CREATE TABLE tbl_employee( id int(11) NOT NULL AUTO_INCREMENT,
last_name varchar(50) DEFAULT NULL,
email varchar(50) DEFAULT NULL,
gender char(1) DEFAULT NULL,
age int(11) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
<dependencies>
<!-- MP -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>2.3</version>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- 数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.39</version>
</dependency>
<!-- Spring 相关 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
</dependencies>
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd"><!-- 不作任何配置 --><configuration />
jdbc.url=jdbc:mysql://localhost:3306/mp
jdbc.username=mp
jdbc.password=mp
<!-- 数据源 -->
<context:property-placeholder location="classpath:db.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!-- MP 提供的 MybatisSqlSessionFactoryBean -->
<bean id="sqlSessionFactoryBean"
class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean">
<!-- 数据源 -->
<property name="dataSource" ref="dataSource"></property>
<!-- mybatis 全局配置文件 -->
<property name="configLocation" value="classpath:mybatis-config.xml"></property>
<!-- 别名处理 -->
<property name="typeAliasesPackage" value="com.jas.bean"></property>
<!-- 注入全局MP策略配置 -->
<property name="globalConfig" ref="globalConfiguration"></property>
<!-- 插件注册 -->
<property name="plugins">
<list>
<!-- 注册分页插件 -->
<bean class="com.baomidou.mybatisplus.plugins.PaginationInterceptor" />
<!-- 注入 SQL 性能分析插件,建议在开发环境中使用,可以在控制台查看 SQL 执行日志 -->
<bean class="com.baomidou.mybatisplus.plugins.PerformanceInterceptor">
<property name="maxTime" value="1000" />
<!--SQL 是否格式化 默认false-->
<property name="format" value="true" />
</bean>
</list>
</property>
</bean>
<!-- 定义 MybatisPlus 的全局策略配置-->
<bean id ="globalConfiguration" class="com.baomidou.mybatisplus.entity.GlobalConfiguration">
<!-- 在 2.3 版本以后,dbColumnUnderline 默认值是 true -->
<property name="dbColumnUnderline" value="true"></property>
<!-- 全局的主键策略 -->
<property name="idType" value="0"></property>
<!-- 全局的表前缀策略配置 -->
<property name="tablePrefix" value="tbl_"></property>
</bean>
<!-- 配置mybatis 扫描mapper接口的路径 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.jas.mapper"></property>
</bean>
MyBatis Plus使用示例
@TableName(value = "tbl_employee")public class Employee { @TableId(value = "id", type = IdType.AUTO)
private Integer id; @TableField(value = "last_name")
private String lastName; private String email; private Integer gender; private Integer age; public Employee() { super();
}
public Employee(Integer id, String lastName, String email, Integer gender, Integer age) { this.id = id; this.lastName = lastName; this.email = email; this.gender = gender; this.age = age;
} // 省略 set、get 与 toString() 方法
/**
* 不定义任何接口方法
*/public interface EmployeeMapper extends BaseMapper<Employee> {}
private ApplicationContext context =
new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
private EmployeeMapper employeeMapper =
context.getBean("employeeMapper", EmployeeMapper.class);
@Test
public void getEmpByIdTest() {
Employee employee = employeeMapper.selectById(1);
System.out.println(employee);
}
@Test
public void getEmpByPage() {
Page<?> page = new Page<>(1, 5);
List<Employee> list = employeeMapper.selectPage(page, null);
System.out.println("总记录数:" + page.getTotal());
System.out.println("总页数" + page.getPages());
System.out.println(list);
}
@Test
public void getEmpByName() {
EntityWrapper<Employee> wrapper = new EntityWrapper<>();
// 'last_name' 与 'age' 对应数据库中的字段
wrapper.like("last_name", "张");
wrapper.eq("age", 20);
List<Employee> list = employeeMapper.selectList(wrapper);
System.out.println(list);
}
控制台输出的SQL分析日志
简单的数据库操作不需要在 EmployeeMapper 接口中定义任何方法,也没有在配置文件中编写SQL语句,而是通过继承BaseMapper接口获得通用的的增删改查方法,复杂的SQL也可以使用条件构造器拼接.不过复杂的业务需求还是要编写SQL语句的,流程和MyBatis一样.
MyBatis Plus使用场景
代码生成器
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
<scope>test</scope></dependency>
public class MysqlGenerator { private static final String PACKAGE_NAME = "cn.lqdev.learning.springboot.chapter9"; private static final String MODULE_NAME = "biz"; private static final String OUT_PATH = "D:\\develop\\code"; private static final String AUTHOR = "oKong"; private static final String DRIVER = "com.mysql.jdbc.Driver"; private static final String URL = "jdbc:mysql://127.0.0.1:3306/learning?useUnicode=true&characterEncoding=UTF-8"; private static final String USER_NAME = "root"; private static final String PASSWORD = "123456"; /**
* <p>
* MySQL 生成演示
* </p>
*/
public static void main(String[] args) { // 自定义需要填充的字段
List<TableFill> tableFillList = new ArrayList<TableFill>(); // 代码生成器
AutoGenerator mpg = new AutoGenerator().setGlobalConfig( // 全局配置
new GlobalConfig().setOutputDir(OUT_PATH)// 输出目录
.setFileOverride(true)// 是否覆盖文件
.setActiveRecord(true)// 开启 activeRecord 模式
.setEnableCache(false)// XML 二级缓存
.setBaseResultMap(false)// XML ResultMap
.setBaseColumnList(true)// XML columList
.setAuthor(AUTHOR) // 自定义文件命名,注意 %s 会自动填充表实体属性!
.setXmlName("%sMapper").setMapperName("%sDao") // .setServiceName("MP%sService")
// .setServiceImplName("%sServiceDiy")
// .setControllerName("%sAction")
).setDataSource( // 数据源配置
new DataSourceConfig().setDbType(DbType.MYSQL)// 数据库类型
.setTypeConvert(new MySqlTypeConvert() { // 自定义数据库表字段类型转换【可选】
@Override
public DbColumnType processTypeConvert(String fieldType) {
System.out.println("转换类型:" + fieldType); // if ( fieldType.toLowerCase().contains( "tinyint" ) ) {
// return DbColumnType.BOOLEAN;
// }
return super.processTypeConvert(fieldType);
}
}).setDriverName(DRIVER).setUsername(USER_NAME).setPassword(PASSWORD).setUrl(URL))
.setStrategy( // 策略配置
new StrategyConfig() // .setCapitalMode(true)// 全局大写命名
.setDbColumnUnderline(true)// 全局下划线命名
// .setTablePrefix(new String[]{"unionpay_"})// 此处可以修改为您的表前缀
.setNaming(NamingStrategy.underline_to_camel)// 表名生成策略
// .setInclude(new String[] {"citycode_org"}) // 需要生成的表
// .setExclude(new String[]{"test"}) // 排除生成的表
// 自定义实体,公共字段
// .setSuperEntityColumns(new String[]{"test_id"})
.setTableFillList(tableFillList) // 自定义实体父类
// .setSuperEntityClass("com.baomidou.demo.common.base.BsBaseEntity")
// // 自定义 mapper 父类
// .setSuperMapperClass("com.baomidou.demo.common.base.BsBaseMapper")
// // 自定义 service 父类
// .setSuperServiceClass("com.baomidou.demo.common.base.BsBaseService")
// // 自定义 service 实现类父类
// .setSuperServiceImplClass("com.baomidou.demo.common.base.BsBaseServiceImpl")
// 自定义 controller 父类
// .setSuperControllerClass("com.baomidou.demo.TestController")
// 【实体】是否生成字段常量(默认 false)
// public static final String ID = "test_id";
.setEntityColumnConstant(true) // 【实体】是否为构建者模型(默认 false)
// public User setName(String name) {this.name = name; return this;}
.setEntityBuilderModel(true) // 【实体】是否为lombok模型(默认 false)<a href="https://projectlombok.org/">document</a>
.setEntityLombokModel(true) // Boolean类型字段是否移除is前缀处理
// .setEntityBooleanColumnRemoveIsPrefix(true)
// .setRestControllerStyle(true)
// .setControllerMappingHyphenStyle(true)
).setPackageInfo( // 包配置
new PackageConfig().setModuleName(MODULE_NAME).setParent(PACKAGE_NAME)// 自定义包路径
.setController("controller")// 这里是控制器包名,默认 web
.setXml("mapper").setMapper("dao")
).setCfg( // 注入自定义配置,可以在 VM 中使用 cfg.abc 设置的值
new InjectionConfig() { @Override
public void initMap() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("abc", this.getConfig().getGlobalConfig().getAuthor() + "-mp"); this.setMap(map);
}
}.setFileOutConfigList(
Collections.<FileOutConfig>singletonList(new FileOutConfig("/templates/mapper.xml.vm") { // 自定义输出文件目录
@Override
public String outputFile(TableInfo tableInfo) { return OUT_PATH + "/xml/" + tableInfo.getEntityName() + "Mapper.xml";
}
})))
.setTemplate( // 关闭默认 xml 生成,调整生成 至 根目录
new TemplateConfig().setXml(null) // 自定义模板配置,模板可以参考源码 /mybatis-plus/src/main/resources/template 使用 copy
// 至您项目 src/main/resources/template 目录下,模板名称也可自定义如下配置:
// .setController("...");
// .setEntity("...");
// .setMapper("...");
// .setXml("...");
// .setService("...");
// .setServiceImpl("...");
); // 执行生成
mpg.execute();
}
}
通用CRUD
@RunWith(SpringRunner.class)//SpringBootTest 是springboot 用于测试的注解,可指定启动类或者测试环境等,这里直接默认。@SpringBootTest @Slf4jpublic class GeneralTest { @Autowired
IUserService userService; @Test
public void testInsert() {
User user = new User();
user.setCode("001");
user.setName("okong-insert"); //默认的插入策略为:FieldStrategy.NOT_NULL,即:判断 null
//对应在mapper.xml时写法为:<if test="field!=null">
//这个可以修改的,设置字段的@TableField(strategy=FieldStrategy.NOT_EMPTY)
//所以这个时候,为null的字段是不会更新的,也可以开启性能插件,查看sql语句就可以知道
userService.insert(user); //新增所有字段,
userService.insertAllColumn(user);
log.info("新增结束");
} @Test
public void testUpdate() {
User user = new User();
user.setCode("101");
user.setName("oKong-insert"); //这就是ActiveRecord的功能
user.insert(); //也可以直接 userService.insert(user);
//更新
User updUser = new User();
updUser.setId(user.getId());
updUser.setName("okong-upd");
updUser.updateById();
log.info("更新结束");
} @Test
public void testDelete() {
User user = new User();
user.setCode("101");
user.setName("oKong-delete");
user.insert(); //删除
user.deleteById();
log.info("删除结束");
} @Test
public void testSelect() {
User user = new User();
user.setCode("201");
user.setName("oKong-selecdt");
user.insert();
log.info("查询:{}",user.selectById());
}
}
条件构造器
条件构造器主要提供了实体包装器,用于处理SQL语句拼接,排序,实体参数查询:使用的是数据库字段,不是Java属性
@RunWith(SpringRunner.class)//SpringBootTest 是springboot 用于测试的注解,可指定启动类或者测试环境等,这里直接默认。@SpringBootTest @Slf4jpublic class ConditionTest { @Autowired
IUserService userService; @Test
public void testOne() {
User user = new User();
user.setCode("701");
user.setName("okong-condition");
user.insert();
EntityWrapper<User> qryWrapper = new EntityWrapper<>();
qryWrapper.eq(User.CODE, user.getCode());
qryWrapper.eq(User.NAME, user.getName()); //也可以直接 // qryWrapper.setEntity(user);
//打印sql语句
System.out.println(qryWrapper.getSqlSegment()); //设置select 字段 即:select code,name from
qryWrapper.setSqlSelect(User.CODE,User.NAME);
System.out.println(qryWrapper.getSqlSelect()); //查询
User qryUser = userService.selectOne(qryWrapper);
System.out.println(qryUser);
log.info("拼接一结束");
} @Test
public void testTwo() {
User user = new User();
user.setCode("702");
user.setName("okong-condition");
user.insert();
EntityWrapper<User> qryWrapper = new EntityWrapper<>();
qryWrapper.where("code = {0}", user.getCode())
.and("name = {0}",user.getName())
.andNew("status = 0");
System.out.println(qryWrapper.getSqlSegment()); //等等很复杂的。
//复杂的建议直接写在xml里面了,要是非动态的话 比较xml一眼看得懂呀
//查询
User qryUser = userService.selectOne(qryWrapper);
System.out.println(qryUser);
log.info("拼接二结束");
}
}
MyBatis Plus提供的条件构造方法com.baomidou.mybatisplus.mapper.Wrapper
/**
*
* @param rowBounds 分页对象 直接传入page即可
* @param wrapper 条件构造器
* @return
*/
List<User> selectUserWrapper(RowBounds rowBounds, @Param("ew") Wrapper<User> wrapper);
UserMapper.xml加入对应的xml节点:
<!-- 条件构造器形式 -->
<select id="selectUserWrapper" resultType="user">
SELECT <include refid="Base_Column_List" />
FROM USER <where>
${ew.sqlSegment} </where>
</select>
自定义SQL使用条件构造器测试类:
@Test
public void testCustomSql() {
User user = new User();
user.setCode("703");
user.setName("okong-condition");
user.insert();
EntityWrapper<User> qryWrapper = new EntityWrapper<>();
qryWrapper.eq(User.CODE, user.getCode());
Page<User> pageUser = new Page<>();
pageUser.setCurrent(1);
pageUser.setSize(10);
List<User> userlist = userDao.selectUserWrapper(pageUser, qryWrapper);
System.out.println(userlist.get(0));
log.info("自定义sql结束");
}
/**
*
* @param rowBounds 分页对象 直接传入page即可
* @param wrapper 条件构造器
* @return
*/
List<User> selectUserWrapper(RowBounds rowBounds, @Param("ew") Wrapper<User> wrapper);
UserMapper.xml:
<!-- 条件构造器形式 -->
<select id="selectUserWrapper" resultType="user">
SELECT <include refid="Base_Column_List" />
FROM USER <where>
${ew.sqlSegment} </where>
</select>
查询方式
使用说明
|
|
setSqlSelect | 设置SELECT查询字段 |
where | WHERE语句,拼接+WHERE条件 |
and | AND语句,拼接+AND 字段=值 |
andNew | AND 语句,拼接+AND(字段=值) |
or | OR 语句,拼接+OR 字段=值 |
orNew | OR 语句,拼接+OR(字段=值) |
eq | 等于= |
allEq | 基于map内容等于= |
ne | 不等于<> |
gt | 大于> |
ge | 大于等于>= |
lt | 小于< |
le | 小于等于<= |
like | 模糊查询 LIKE |
notLike | 模糊查询NOT LIKE |
in | IN 查询 |
notIn | NOT IN查询 |
isNull | NULL值查询 |
isNotNull | IS NOT NULL |
groupBy | 分组GROUP BY |
having | HAVING关键词 |
orderBy | 排序ORDER BY |
orderAsc | 排序ASC ORDER BY |
orderDesc | 排序DESC ORDER BY |
exists | EXISTS条件语句 |
notExists | NOT EXISTS条件语句 |
between | BETWEEN条件语句 |
notBetween | NOT BETWEEN条件语句 |
addFilter | 自由拼接SQL |
last | 拼接在最后 |
自定义SQL语句
在多表关联时,条件构造器和通用CURD都无法满足时,可以编写SQL语句进行扩展.这些都是mybatis的用法.首先改造UserDao接口,有两种方式:
@Select("SELECT * FROM USER WHERE CODE = #{userCode}")
List<User> selectUserCustomParamsByAnno(@Param("userCode")String userCode);
List<User> selectUserCustomParamsByXml(@Param("userCode")String userCode);
UserMapper.xml新增一个节点:
<!-- 由于设置了别名:typeAliasesPackage=cn.lqdev.learning.mybatisplus.samples.biz.entity,所以resultType可以不写全路径了。 -->
<select id="selectUserCustomParamsByXml" resultType="user">
SELECT
<include refid="Base_Column_List"/>
FROM USER
WHERE CODE = #{userCode} </select>
自定义SQL语句测试类CustomSqlTest:
@RunWith(SpringRunner.class)//SpringBootTest 是springboot 用于测试的注解,可指定启动类或者测试环境等,这里直接默认。@SpringBootTest @Slf4jpublic class CustomSqlTest { @Autowired
UserDao userDao; @Test
public void testCustomAnno() {
User user = new User();
user.setCode("901");
user.setName("okong-sql");
user.insert();
List<User> userlist = userDao.selectUserCustomParamsByAnno(user.getCode()); //由于新增的 肯定不为null 故不判断了。
System.out.println(userlist.get(0).toString());
log.info("注解形式结束------");
} @Test
public void testCustomXml() {
User user = new User();
user.setCode("902");
user.setName("okong-sql");
user.insert();
List<User> userlist = userDao.selectUserCustomParamsByXml(user.getCode()); //由于新增的 肯定不为null 故不判断了。
System.out.println(userlist.get(0).toString());
log.info("xml形式结束------");
}
}
注意:
在使用spring-boot-maven-plugin插件打包成springboot运行jar时,需要注意:由于springboot的jar扫描路径方式问题,会导致别名的包未扫描到,所以这个只需要把mybatis默认的扫描设置为Springboot的VFS实现.修改spring-mybatis.xml文件:
<!--mybatis-->
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- 自动扫描mapper.xml文件,支持通配符 -->
<property name="mapperLocations" value="classpath:mapper/**/*.xml"/>
<!-- 配置文件,比如参数配置(是否启动驼峰等)、插件配置等 -->
<property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/>
<!-- 启用别名,这样就无需写全路径类名了,具体可自行查阅资料 -->
<property name="typeAliasesPackage" value="cn.lqdev.learning.mybatisplus.samples.biz.entity"/>
<!-- MP 全局配置注入 -->
<property name="globalConfig" ref="globalConfig"/>
<!-- 设置vfs实现,避免路径扫描问题 -->
<property name="vfs" value="com.baomidou.mybatisplus.spring.boot.starter.SpringBootVFS"></property>
</bean>
分页插件,性能分析插件
mybatis的插件机制使用只需要注册即可
<plugins>
<!-- SQL 执行性能分析,开发环境使用,线上不推荐。 -->
<plugin interceptor="com.baomidou.mybatisplus.plugins.PerformanceInterceptor"></plugin>
<!-- 分页插件配置 -->
<plugin interceptor="com.baomidou.mybatisplus.plugins.PaginationInterceptor"></plugin>
</plugins>
@RunWith(SpringRunner.class)//SpringBootTest 是springboot 用于测试的注解,可指定启动类或者测试环境等,这里直接默认。@SpringBootTest @Slf4jpublic class PluginTest { @Autowired
IUserService userService; @Test
public void testPagination() {
Page<User> page = new Page<>(); //每页数
page.setSize(10); //当前页码
page.setCurrent(1); //无条件时
Page<User> pageList = userService.selectPage(page);
System.out.println(pageList.getRecords().get(0)); //新增数据 避免查询不到数据
User user = new User();
user.setCode("801");
user.setName("okong-Pagination");
user.insert(); //加入条件构造器
EntityWrapper<User> qryWapper = new EntityWrapper<>(); //这里也能直接设置 entity 这是条件就是entity的非空字段值了// qryWapper.setEntity(user);
//这里建议直接用 常量
// qryWapper.eq(User.CODE, user.getCode());
pageList = userService.selectPage(page, qryWapper);
System.out.println(pageList.getRecords().get(0));
log.info("分页结束");
}
}
Time:4 ms - ID:cn.lqdev.learning.mybatisplus.samples.biz.dao.UserDao.selectPage
Execute SQL: SELECT id AS id,code,`name`,`status`,gmt_create AS gmtCreate,gmt_modified AS gmtModified FROM user WHERE id=1026120705692434433 AND code='801' AND `name`='okong-Pagination' LIMIT 0,10
公共字段自动填充
通常,每个公司都有自己的表定义,在《阿里巴巴Java开发手册》中,就强制规定表必备三字段:id,gmt_create,gmt_modified.所以通常我们都会写个公共的拦截器去实现自动填充比如创建时间和更新时间的,无需开发人员手动设置.而在MP中就提供了这么一个公共字段自动填充功能
设置填充字段的填充类型:
User
注意
可以在代码生成器里面配置规则的,可自动配置
/**
* 创建时间
*/
@TableField(fill=FieldFill.INSERT)
private Date gmtCreate; /**
* 修改时间
*/
@TableField(fill=FieldFill.INSERT_UPDATE)
private Date gmtModified;
定义处理类:
MybatisObjectHandler
public class MybatisObjectHandler extends MetaObjectHandler{ @Override
public void insertFill(MetaObject metaObject) { //新增时填充的字段
setFieldValByName("gmtCreate", new Date(), metaObject);
setFieldValByName("gmtModified", new Date(), metaObject);
} @Override
public void updateFill(MetaObject metaObject) { //更新时 需要填充字段
setFieldValByName("gmtModified", new Date(), metaObject);
}
}
<bean id="globalConfig" class="com.baomidou.mybatisplus.entity.GlobalConfiguration">
<!--
AUTO->`0`("数据库ID自增")QW
INPUT->`1`(用户输入ID")
ID_WORKER->`2`("全局唯一ID")
UUID->`3`("全局唯一ID")
-->
<property name="idType" value="2" />
<property name="metaObjectHandler" ref="mybatisObjectHandler"></property>
</bean>
<bean id="mybatisObjectHandler" class="cn.lqdev.learning.mybatisplus.samples.config.MybatisObjectHandler"/>
再新增或者修改时,对应时间就会进行更新:
Time:31 ms - ID:cn.lqdev.learning.mybatisplus.samples.biz.dao.UserDao.insert
Execute SQL: INSERT INTO user ( id, code, `name`, gmt_create,gmt_modified ) VALUES ( 1026135016838037506, '702', 'okong-condition', '2018-08-05 23:57:07.344','2018-08-05 23:57:07.344' )
数据库连接池-Alibaba Druid
配置参数
缺省值
说明
|
|
|
name |
| 如果存在多个数据源,监控时可以通过name属性进行区分,如果没有配置,将会生成一个名字:"DataSource-"+System.identityHashCode(this) |
jdbcUrl |
| 连接数据库的url,不同的数据库url表示方式不同: mysql:jdbc:mysql://192.16.32.128:3306/druid2 oracle : jdbc:oracle:thin:@192.16.32.128:1521:druid2 |
username |
| 连接数据库的用户名 |
password |
| 连接数据库的密码,密码不出现在配置文件中可以使用ConfigFilter |
driverClassName | 根据jdbcUrl自动识别 | 可以不配置,Druid会根据jdbcUrl自动识别dbType,选择相应的driverClassName |
initialSize | 0 | 初始化时建立物理连接的个数. 初始化过程发生在:显示调用init方法;第一次getConnection |
maxActive | 8 | 最大连接池数量 |
minIdle |
| 最小连接池数量 |
maxWait |
| 获取连接时最大等待时间,单位毫秒. 配置maxWait默认使用公平锁等待机制,并发效率会下降.可以配置useUnfairLock为true使用非公平锁 |
poolPreparedStatements | false | 是否缓存preparedStatement,即PSCache. PSCache能够提升对支持游标的数据库性能. 在Oracle中使用,在MySQL中关闭 |
maxOpenPreparedStatements | -1 | 要启用PSCache,必须配置参数值>0,poolPreparedStatements自动触发修改为true. Oracle中可以配置数值为100,Oracle中不会存在PSCache过多的问题 |
validationQuery |
| 用来检测连接的是否为有效SQL,要求是一个查询语句 如果validationQuery=null,那么testOnBorrow,testOnReturn,testWhileIdle都不会起作用 |
testOnBorrow | true | 申请连接时执行validationQuery检测连接是否有效,会降低性能 |
testOnReturn | false | 归还连接时执行validationQuery检测连接是否有效,会降低性能 |
testWhileIdle | false | 申请连接时,空闲时间大于timeBetweenEvictionRunsMillis时,执行validationQuery检测连接是否有效 不影响性能,保证安全性,建议配置为true |
timeBetweenEvictionRunsMillis |
| Destroy线程会检测连接的间隔时间 testWhileIdle的判断依据 |
connectionInitSqls |
| 物理连接初始化时执行SQL |
exceptionSorter | 根据dbType自动识别 | 当数据库跑出不可恢复的异常时,抛弃连接 |
filters |
| 通过别名的方式配置扩展插件,属性类型是字符串: 常用的插件: 监控统计用的filter:stat 日志用的filter:log4j 防御sql注入的filter:wall |
proxyFilters |
| 类型是List<com.alibaba.druid.filter.Filter>,如果同时配置了filters和proxyFilters是组合关系,不是替换关系 |
Druid的架构
Druid数据结构
Druid架构相辅相成的是基于DataSource和Segment的数据结构
DataSource数据结构: 是逻辑概念, 与传统的关系型数据库相比较DataSource可以理解为表
时间列: 表明每行数据的时间值
维度列: 表明数据的各个维度信息
指标列: 需要聚合的列的数据
Segment结构: 实际的物理存储格式,
Druid通过Segment实现了横纵向切割操作
Druid将不同的时间范围内的数据存放在不同的Segment文件块中,通过时间实现了横向切割
Segment也面向列进行数据压缩存储,实现纵向切割
Druid架构包含四个节点和一个服务:
实时节点(RealTime Node): 即时摄入实时数据,并且生成Segment文件
历史节点(Historical Node): 加载已经生成好的数据文件,以供数据查询使用
查询节点(Broker Node): 对外提供数据查询服务,并且从实时节点和历史节点汇总数据,合并后返回
协调节点( Coordinator Node): 负责历史节点的数据的负载均衡,以及通过规则管理数据的生命周期
索引服务(Indexing Service): 有不同的获取数据的方式,更加灵活的生成segment文件管理资源
实时节点
主要负责即时摄入实时数据,以及生成Segment文件
实时节点通过firehose进行数据的摄入,firehose是Druid实时消费模型
通过kafka消费,就是kafkaFireHose.
同时,实时节点的另外一个模块Plumer,用于Segment的生成,并且按照指定的周期,
将本周期内生成的所有数据块合并成一个
1.实时节点生产出Segment文件,并且存到文件系统中2.Segment文件的<MetaStore>存放到Mysql等其他外部数据库中3.Master通过Mysql中的MetaStore,通过一定的规则,将Segment分配给属于它的节点4.历史节点得到Master发送的指令后会从文件系统中拉取属于自己的Segment文件,并且通过zookeeper,告知集群,自己提供了此块Segment的查询服务5.实时节点丢弃Segment文件,并且声明不在提供此块文件的查询服务
历史节点
历史节点再启动的时候:
优先检查自己的本地缓存中是否已经有了缓存的Segment文件
然后从文件系统中下载属于自己,但还不存在的Segment文件
无论是何种查询,历史节点首先将相关的Segment从磁盘加载到内存.然后再提供服务
历史节点的查询效率受内存空间富余程度的影响很大:
内存空间富余,查询时需要从磁盘加载数据的次数减少,查询速度就快
内存空间不足,查询时需要从磁盘加载数据的次数就多,查询速度就相对较慢
原则上历史节点的查询速度与其内存大小和所负责的Segment数据文件大小成正比关系
查询节点
协调节点
对于整个Druid集群来说,其实并没有真正意义上的Master节点.
实时节点与查询节点能自行管理并不听命于任何其他节点,
对于历史节点来说,协调节点便是他们的Master,因为协调节点将会给历史节点分配数据,完成数据分布在历史节点之间的负载均衡.
历史节点之间是相互不进行通讯的,全部通过协调节点进行通讯
利用规则管理数据的生命周期:
Druid利用针对每个DataSoure设置的规则来加载或者丢弃具体的文件数据,来管理数据的生命周期
可以对一个DataSource按顺序添加多条规则,对于一个Segment文件来说,协调节点会逐条检查规则
当碰到当前Segment文件负责某条规则的情况下,协调节点会立即命令历史节点对该文件执行此规则,加载或者丢弃,并停止余下的规则,否则继续检查
索引服务
除了通过实时节点生产Segment文件之外,druid还提供了一组索引服务来摄入数据
Middle Manager与Peon(苦工):Middle Manager即是Overload node 的工作节点,负责接收Overload node分配的任务,
然后启动相关的Peon来完成任务这种模式和yarn的架构比较类似
1.Overload node相当于Yarn的ResourceManager,负责资源管理和任务分配
2.Middle Manager相当于Yarn的NodeManager,负责管理独立节点的资源,并且接收任务
3.Peon 相当于Yarn的Container,启动在具体节点上具体任务的执行
网关-Zuul
Zuul是netflix开源的一个API Gateway 服务器, 本质上是一个web servlet应用
-Zuul是一个基于JVM路由和服务端的负载均衡器,提供动态路由,监控,弹性,安全等边缘服务的框架,相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门
Zuul工作原理
过滤器机制
1.Zuul的过滤器之间没有直接的相互通信,他们之间通过一个RequestContext的静态类来进行数据传递的。RequestContext类中有ThreadLocal变量来记录每个Request所需要传递的数据
2.Zuul的过滤器是由Groovy写成,这些过滤器文件被放在Zuul Server上的特定目录下面,Zuul会定期轮询这些目录,修改过的过滤器会动态的加载到Zuul Server中以便过滤请求使用
StaticResponseFilter: StaticResponseFilter允许从Zuul本身生成响应,而不是将请求转发到源
SurgicalDebugFilter: SurgicalDebugFilter允许将特定请求路由到分隔的调试集群或主机
PRE: 在请求被路由之前调用,利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等
ROUTING: 请求路由到微服务,用于构建发送给微服务的请求,使用Apache HttpClient或Netfilx Ribbon请求微服务
POST: 在路由到微服务以后执行,用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等
ERROR: 在其他阶段发生错误时执行该过滤器
标准过滤器类型:
Zuul大部分功能都是通过过滤器来实现的。Zuul中定义了四种标准过滤器类型,这些过滤器类型对应于请求的典型生命周期
内置的特殊过滤器:
自定义的过滤器:
除了默认的过滤器类型,Zuul还允许我们创建自定义的过滤器类型。如STATIC类型的过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务
Zuul提供了一个框架,可以对过滤器进行动态的加载,编译,运行
过滤器的生命周期
Zuul请求的生命周期详细描述了各种类型的过滤器的执行顺序
过滤器调度过程
动态加载过滤器
Zuul的作用
Zuul可以通过加载动态过滤机制实现Zuul的功能:
验证与安全保障: 识别面向各类资源的验证要求并拒绝那些与要求不符的请求
审查与监控: 在边缘位置追踪有意义数据及统计结果,得到准确的生产状态结论
动态路由: 以动态方式根据需要将请求路由至不同后端集群处
压力测试: 逐渐增加指向集群的负载流量,从而计算性能水平
负载分配: 为每一种负载类型分配对应容量,并弃用超出限定值的请求
静态响应处理: 在边缘位置直接建立部分响应,从而避免其流入内部集群
多区域弹性: 跨越AWS区域进行请求路由,旨在实现ELB使用多样化并保证边缘位置与使用者尽可能接近
Zuul与应用的集成方式
缓存-Redis
Redis: Redis是一个开源的内存中的数据结构存储系统,可以用作数据库,缓存和消息中间件
操作工具:Redis Desktop Manager
整合Redis缓存
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId></dependency>
spring.redis.host=192.168.32.242
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory); return template;
}
@Configurationpublic class MyRedisConfig { @Bean
public RedisTemplate<Object, Employee> empRedisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<Object,Employee> redisTemplate=new RedisTemplate<Object,Employee>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<Employee> serializer = new Jackson2JsonRedisSerializer<Employee>(Employee.class);
redisTemplate.setDefaultSerializer(serializer); return redisTemplate;
}
}
Redis常见的数据类型: String-字符串 List-列表 Set-集合 Hash-散列 ZSet-有序集合redisTemplate.opsForValue()--String(字符串)redisTemplate.opsForList()--List(列表)redisTemplate.opsForSet()--Set(集合)redisTemplate.opsForHash()--Hash(散列)redisTemplate.opsForZSet()--ZSet(有序集合)
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory); return template;
}
在StringRedisTemplate中:
public class StringRedisTemplate extends RedisTemplate<String, String> { public StringRedisTemplate() { this.setKeySerializer(RedisSerializer.string()); this.setValueSerializer(RedisSerializer.string()); this.setHashKeySerializer(RedisSerializer.string()); this.setHashValueSerializer(RedisSerializer.string());
} public StringRedisTemplate(RedisConnectionFactory connectionFactory) { this(); this.setConnectionFactory(connectionFactory); this.afterPropertiesSet();
} protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) { return new DefaultStringRedisConnection(connection);
}
}
Redis常见的数据类型: String-字符串 List-列表 Set-集合 Hash-散列 ZSet-有序集合stringRedisTemplate.opsForValue()--String(字符串)stringRedisTemplate.opsForList()--List(列表)stringRedisTemplate.opsForSet()--Set(集合)stringRedisTemplate.opsForHash()--Hash(散列)stringRedisTemplate.opsForZSet()--ZSet(有序集合)
注册中心-Zookeeper,Eureka
Zookeeper基本概念
Zookeeper是一个分布式的,开放源码的分布式应用程序协调服务
Zookeeper是hadoop的一个子项目
包含一个简单的原语集, 分布式应用程序可以基于它实现同步服务,配置维护和命名服务等
在分布式应用中,由于工程师不能很好地使用锁机制,以及基于消息的协调机制不适合在某些应用中使用,Zookeeper提供一种可靠的,可扩展的,分布式的,可配置的协调机制来统一系统的状态
Zookeeper中的角色:
系统模型图:
Zookeeper特点:
最终一致性: client不论连接到哪个Server,展示给它都是同一个视图,这是Zookeeper最重要的性能
可靠性: 具有简单,健壮,良好的性能,如果消息m被到一台服务器接受,那么它将被所有的服务器接受
实时性: Zookeeper保证客户端将在一个时间间隔范围内获得服务器的更新信息,或者服务器失效的信息.但由于网络延时等原因,Zookeeper不能保证两个客户端能同时得到刚更新的数据,如果需要最新数据,应该在读数据之前调用sync()接口
等待无关(wait-free): 慢的或者失效的client不得干预快速的client的请求,使得每个client都能有效的等待
原子性: 更新只能成功或者失败,没有中间状态
顺序性: 包括全局有序和偏序两种:全局有序是指如果在一台服务器上消息a在消息b前发布,则在所有Server上消息a都将在消息b前被发布.偏序是指如果一个消息b在消息a后被同一个发送者发布,a必将排在b前面
Zookeeper工作原理
Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步实现这个机制的协议叫做Zab协议
Zab协议有两种模式:恢复模式(选主),广播模式(同步)
当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和leader的状态同步以后,恢复模式就结束了
状态同步保证了leader和Server具有相同的系统状态
为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务
所有的提议(proposal)都在被提出的时候加上了zxid.实现中zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,标识当前属于那个leader的统治时期.低32位用于递增计数
每个Server在工作过程中有三种状态:
LOOKING: 当前Server不知道leader是谁,正在搜寻
LEADING: 当前Server即为选举出来的leader
FOLLOWING: leader已经选举出来,当前Server与之同步
选主流程
当leader崩溃或者leader失去大多数的follower这时候Zookeeper进入恢复模式
恢复模式需要重新选举出一个新的leader,让所有的Server都恢复到一个正确的状态.
Zookeeper的选举算法有两种:系统默认的选举算法为fast paxos
基于fast paxos算法
基于basic paxos算法
基于fast paxos算法:
fast paxos流程是在选举过程中,某Server首先向所有Server提议自己要成为leader,当其它Server收到提议以后,解决epoch和zxid的冲突,并接受对方的提议,然后向对方发送接受提议完成的消息,重复这个流程,最后一定能选举出Leader
基于basic paxos算法:
选举线程由当前Server发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐的Server
选举线程首先向所有Server发起一次询问(包括自己)
选举线程收到回复后,验证是否是自己发起的询问(验证zxid是否一致),然后获取对方的id(myid),并存储到当前询问对象列表中,最后获取对方提议的leader相关信息(id,zxid),并将这些信息存储到当次选举的投票记录表中
收到所有Server回复以后,就计算出zxid最大的那个Server,并将这个Server相关信息设置成下一次要投票的Server;
线程将当前zxid最大的Server设置为当前Server要推荐的Leader,如果此时获胜的Server获得n/2+1的Server票数,设置当前推荐的leader为获胜的Server,将根据获胜的Server相关信息设置自己的状态,否则,继续这个过程,直到leader被选举出来
通过流程分析我们可以得出:要使Leader获得多数Server的支持,则Server总数必须是奇数2n+1,且存活的Server的数目不得少于n+1.每个Server启动后都会重复以上流程.在恢复模式下,如果是刚从崩溃状态恢复的或者刚启动的server还会从磁盘快照中恢复数据和会话信息,Zookeeper会记录事务日志并定期进行快照,方便在恢复时进行状态恢复.选主的具体流程图如下所示:
同步流程
工作流程
Leader工作流程:
Leader主要有三个功能:
PING消息: Learner的心跳信息
REQUEST消息: Follower发送的提议信息,包括写请求及同步请求
ACK消息: Follower的对提议的回复.超过半数的Follower通过,则commit该提议
REVALIDATE消息: 用来延长SESSION有效时间
恢复数据
维持与Learner的心跳,接收Learner请求并判断Learner的请求消息类型
Learner的消息类型主要有PING消息,REQUEST消息,ACK消息,REVALIDATE消息,根据不同的消息类型,进行不同的处理
Leader的工作流程简图如下所示,在实际实现中,流程要比下图复杂得多,启动了三个线程来实现功能:
Follower工作流程:
Follower主要有四个功能:
向Leader发送请求(PING消息,REQUEST消息,ACK消息,REVALIDATE消息)
接收Leader消息并进行处理
接收Client的请求,如果为写请求,发送给Leader进行投票
返回Client结果
Follower的消息循环处理如下几种来自Leader的消息:
PING消息: 心跳消息
PROPOSAL消息: Leader发起的提案,要求Follower投票
COMMIT消息: 服务器端最新一次提案的信息
UPTODATE消息: 表明同步完成
REVALIDATE消息: 根据Leader的REVALIDATE结果,关闭待revalidate的session还是允许其接受消息
SYNC消息: 返回SYNC结果到客户端,这个消息最初由客户端发起,用来强制得到最新的更新
Follower的工作流程简图如下所示,在实际实现中,Follower是通过5个线程来实现功能的:
observer流程和Follower的唯一不同的地方就是observer不会参加leader发起的投票
Zookeeper应用场景
配置管理
集中式的配置管理在应用集群中是非常常见的,一般都会实现一套集中的配置管理中心,应对不同的应用集群对于共享各自配置的需求,并且在配置变更时能够通知到集群中的每一个机器,也可以细分进行分层级监控
Zookeeper很容易实现这种集中式的配置管理,比如将APP1的所有配置配置到/APP1 znode下,APP1所有机器一启动就对/APP1这个节点进行监控(zk.exist("/APP1",true)),并且实现回调方法Watcher,那么在zookeeper上/APP1 znode节点下数据发生变化的时候,每个机器都会收到通知,Watcher方法将会被执行,那么应用再取下数据即可(zk.getData("/APP1",false,null))
集群管理
应用集群中,我们常常需要让每一个机器知道集群中(或依赖的其他某一个集群)哪些机器是活着的,并且在集群机器因为宕机,网络断链等原因能够不在人工介入的情况下迅速通知到每一个机器
Zookeeper同样很容易实现这个功能,比如我在zookeeper服务器端有一个znode叫 /APP1SERVERS, 那么集群中每一个机器启动的时候都去这个节点下创建一个EPHEMERAL类型的节点,比如server1创建/APP1SERVERS/SERVER1(可以使用ip,保证不重复),server2创建/APP1SERVERS/SERVER2,然后SERVER1和SERVER2都watch /APP1SERVERS这个父节点,那么也就是这个父节点下数据或者子节点变化都会通知对该节点进行watch的客户端.因为EPHEMERAL类型节点有一个很重要的特性,就是客户端和服务器端连接断掉或者session过期就会使节点消失,那么在某一个机器挂掉或者断链的时候,其对应的节点就会消失,然后集群中所有对/APP1SERVERS进行watch的客户端都会收到通知,然后取得最新列表即可
另外有一个应用场景就是集群选master: 一旦master挂掉能够马上能从slave中选出一个master,实现步骤和前者一样,只是机器在启动的时候在APP1SERVERS创建的节点类型变为EPHEMERAL_SEQUENTIAL类型,这样每个节点会自动被编号
我们默认规定编号最小的为master,所以当我们对/APP1SERVERS节点做监控的时候,得到服务器列表,只要所有集群机器逻辑认为最小编号节点为master,那么master就被选出,而这个master宕机的时候,相应的znode会消失,然后新的服务器列表就被推送到客户端,然后每个节点逻辑认为最小编号节点为master,这样就做到动态master选举
Zookeeper监视
Zookeeper所有的读操作-getData(),getChildren(),和exists() 都可以设置监视(watch),监视事件可以理解为一次性的触发器. 官方定义如下: a watch event is one-time trigger, sent to the client that set the watch, which occurs when the data for which the watch was set changes:
znode 节点本身具有不同的改变方式
例如:Zookeeper 维护了两条监视链表:数据监视和子节点监视(data watches and child watches) getData() and exists()设置数据监视,getChildren()设置子节点监视
又例如:Zookeeper设置的不同监视返回不同的数据,getData()和exists()返回znode节点的相关信息,而getChildren()返回子节点列表.因此,setData()会触发设置在某一节点上所设置的数据监视(假定数据设置成功),而一次成功的create()操作则会出发当前节点上所设置的数据监视以及父节点的子节点监视.一次成功的delete()操作将会触发当前节点的数据监视和子节点监视事件,同时也会触发该节点父节点的child watch
Zookeeper客户端和服务端是通过socket进行通信的,由于网络存在故障,所以监视事件很有可能不会成功地到达客户端,监视事件是异步发送至监视者的
Zookeeper本身提供了保序性(ordering guarantee):即客户端只有首先看到了监视事件后,才会感知到它所设置监视的znode发生了变化(a client will never see a change for which it has set a watch until it first sees the watch event).网络延迟或者其他因素可能导致不同的客户端在不同的时刻感知某一监视事件,但是不同的客户端所看到的一切具有一致的顺序
当设置监视的数据发生改变时,该监视事件会被发送到客户端
例如:如果客户端调用了getData("/znode1", true)并且稍后/znode1节点上的数据发生了改变或者被删除了,客户端将会获取到/znode1发生变化的监视事件,而如果/znode1再一次发生了变化,除非客户端再次对/znode1设置监视,否则客户端不会收到事件通知
One-time trigger(一次性触发)
Sent to the client(发送至客户端)
The data for which the watch was set(被设置watch的数据)
Zookeeper中的监视是轻量级的,因此容易设置,维护和分发.当客户端与 Zookeeper 服务器端失去联系时,客户端并不会收到监视事件的通知,只有当客户端重新连接后,若在必要的情况下,以前注册的监视会重新被注册并触发,对于开发人员来说这通常是透明的.只有一种情况会导致监视事件的丢失,即:通过exists()设置了某个znode节点的监视,但是如果某个客户端在此znode节点被创建和删除的时间间隔内与zookeeper服务器失去了联系,该客户端即使稍后重新连接zookeeper服务器后也得不到事件通知
Eureka(服务发现框架)
Eureka是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的. SpringCloud将它集成在其子项目spring-cloud-netflix中,以实现SpringCloud的服务发现功能
Eureka的两个组件
Eureka Server: Eureka Server提供服务注册服务,各个节点启动后,会在Eureka Server中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中看到. Eureka Server之间通过复制的方式完成数据的同步
Eureka Client: 是一个java客户端,用于简化与Eureka Server的交互,客户端同时也就是一个内置的、使用轮询(round-robin)负载算法的负载均衡器
Eureka通过心跳检查、客户端缓存等机制,确保了系统的高可用性、灵活性和可伸缩性
在应用启动后,将会向Eureka Server发送心跳, 如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除。
Eureka还提供了客户端缓存机制,即使所有的Eureka Server都挂掉,客户端依然可以利用缓存中的信息消费其他服务的API。Eureka通过心跳检查、客户端缓存等机制,确保了系统的高可用性、灵活性和可伸缩性
作业调度框架-Quartz
Quartz作业调度框架概念
Quartz是一个完全由java编写的开源作业调度框架,是OpenSymphony开源组织在Job scheduling领域的开源项目,它可以与J2EE与J2SE应用程序相结合也可以单独使用,Quartz框架整合了许多额外功能.Quartz可以用来创建简单或运行十个,百个,甚至是好几万个Jobs这样复杂的程序
Quartz三个主要的概念:
调度作业,什么时候开始执行,什么时候结束执行
自己编写的业务逻辑,交给quartz执行
Quartz框架的核心是调度器
调度器负责管理Quartz应用运行时环境
调度器不是靠自己做所有的工作,而是依赖框架内一些非常重要的部件
Quartz怎样能并发运行多个作业的原理: Quartz不仅仅是线程和线程池管理,为确保可伸缩性,Quartz采用了基于多线程的架构.启动时,框架初始化一套worker线程,这套线程被调度器用来执行预定的作业.
Quartz依赖一套松耦合的线程池管理部件来管理线程环境
调度器:
任务:
触发器:
Quartz设计模式
Builer模式
Factory模式
组件模式
链式写法
Quartz体系结构
Quartz框架中的核心类:
JobDetail:
Quartz每次运行都会直接创建一个JobDetail,同时创建一个Job实例.
不直接接受一个Job的实例,接受一个Job的实现类
通过new instance()的反射方式来实例一个Job,在这里Job是一个接口,需要编写类去实现这个接口
Trigger:
Scheduler:
Quartz重要组件
Job接口
public class HelloJob implements Job{ public void execute(JobExecutionContext context) throws JobExecutionException { //编写我们自己的业务逻辑
}
JobDetail
每次都会直接创建一个JobDetail,同时创建一个Job实例,它不直接接受一个Job的实例,但是它接受一个Job的实现类,通过new instance()的反射方式来实例一个Job.可以通过下面的方式将一个Job实现类绑定到JobDetail中
JobDetail jobDetail=JobBuilder.newJob(HelloJob.class).
withIdentity("myJob", "group1")
.build();
JobBuiler
JobStore
Trigger
CronTrigger trigger = (CronTrigger) TriggerBuilder
.newTrigger()
.withIdentity("myTrigger", "group1") //创建一个标识符
.startAt(date)//什么时候开始触发
//每秒钟触发一次任务
.withSchedule(CronScheduleBuilder.cronSchedule("* * * * * ? *"))
.build();
Scheduler
创建Scheduler有两种方式
SchedulerFactory sfact=new StdSchedulerFactory();
Scheduler scheduler=sfact.getScheduler();
DiredtSchedulerFactory factory=DirectSchedulerFactory.getInstance();
Scheduler scheduler=factory.getScheduler();
Scheduler配置参数一般存储在quartz.properties中,我们可以修改参数来配置相应的参数.通过调用getScheduler() 方法就能创建和初始化调度对象
Scheduler的主要函数:
Date schedulerJob(JobDetail,Trigger trigger): 返回最近触发的一次时间
void standby(): 暂时挂起
void shutdown(): 完全关闭,不能重新启动
shutdown(true): 表示等待所有正在执行的job执行完毕之后,再关闭scheduler
shutdown(false): 直接关闭scheduler
quartz.properties资源文件:
在org.quartz这个包下,当我们程序启动的时候,它首先会到我们的根目录下查看是否配置了该资源文件,如果没有就会到该包下读取相应信息,当我们咋实现更复杂的逻辑时,需要自己指定参数的时候,可以自己配置参数来实现
org.quartz.scheduler.instanceName: DefaultQuartzScheduler
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 10
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
org.quartz.jobStore.misfireThreshold: 60000
org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
监听器
对事件进行监听并且加入自己相应的业务逻辑,主要有以下三个监听器分别对Job,Trigger,Scheduler进行监听:
JobListener
TriggerListener
SchedulerListener
Cron表达式
字段
允许值
允许特殊字符
|
|
|
秒 | 0-59 | , - * / |
分 | 0-59 | , - * / |
小时 | 0-23 | , - * / |
日期 | 1-31 | , - * ? / L W C |
月份 | 1-12 | , - * / |
星期 | 0-7或SUN-SAT,0和7是SUN | , - * / |
特殊字符
含义
|
|
, | 枚举 |
- | 区间 |
* | 任意 |
/ | 步长 |
? | 日和星期的冲突匹配 |
L | 最后 |
w | 工作日 |
C | 与calendar联系后计算过的值 |
# | 星期: 4#2-第2个星期三 |
second(秒),minute(分),hour(时),day of month(日),month(月),day of week(周几)0 * * * * MON-FRI@Scheduled(cron="0 * * * * MON-FRI")@Scheduled(cron="1,2,3 * * * * MON-FRI")-枚举: ,@Scheduled(cron="0-15 * * * * MON-FRI")-区间: -@Scheduled(cron="0/4 * * * * MON-FRI")-步长: / 从0开始,每4秒启动一次
cron="0 0/5 14,18 * * ?" 每天14点整和18点整,每隔5分钟执行一次
cron="0 15 10 ? * 1-6" 每个月的周一至周六10:15分执行一次
cron="0 0 2 ? * 6L" 每个月的最后一个周六2点执行一次
cron="0 0 2 LW * ?" 每个月的最后一个工作日2点执行一次
cron="0 0 2-4 ? * 1#1" 每个月的第一个周一2点到4点,每个整点执行一次
接口测试框架-Swagger2
Swagger介绍
Swagger是一款RESTful接口的文档在线生成和接口测试工具
Swagger是一个规范完整的框架,用于生成,描述,调用和可视化RESTful风格的web服务
总体目标是使客户端和文件系统作为服务器以同样的速度更新
文件的方法,参数和模型紧密集成到服务器端代码,允许API始终保持同步
Swagger作用
Swagger主要项目
Swagger-tools: 提供各种与Swagger进行集成和交互的工具. 比如Swagger Inspector,Swagger Editor
Swagger-core: 用于Java或者Scala的Swagger实现,与JAX-RS,Servlets和Play框架进行集成
Swagger-js: 用于JavaScript的Swagger实现
Swagger-node-express: Swagger模块,用于node.js的Express Web应用框架
Swagger-ui: 一个无依赖的html,js和css集合,可以为Swagger的RESTful API动态生成文档
Swagger-codegen: 一个模板驱动引擎,通过分析用户Swagger资源声明以各种语言生成客户端代码
Swagger工具
Swagger Codegen:
通过Codegen可以将描述文件生成html格式和cwiki形式的接口文档,同时也能生成多种语言的服务端和客户端的代码
支持通过jar包 ,docker,node等方式在本地化执行生成,也可以在后面Swagger Editor中在线生成
Swagger UI:
Swagger Editor:
Swagger Inspector:
Swagger Hub:
Swagger注解
@Api
该注解将一个controller类标注为一个Swagger API. 在默认情况下 ,Swagger core只会扫描解析具有 @Api注解的类,而忽略其它类别的资源,比如JAX-RS endpoints, Servlets等注解. 该注解的属性有:
tags: API分组标签,具有相同标签的API将会被归并在一组内显示
value: 如果tags没有定义 ,value将作为Api的tags使用
@ApiOperation
在指定接口路径上,对一个操作或者http方法进行描述. 具有相同路径的不同操作会被归组为同一个操作对象. 紧接着是不同的http请求方法注解和路径组合构成一个唯一操作. 该注解的属性有:
value: 对操作进行简单说明
notes: 对操作进行详细说明
httpMethod: http请求动作名,可选值有 :GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH
code: 成功操作后的返回类型. 默认为200, 参照标准Http Status Code Definitions
@ApiParam
增加对参数的元信息说明,紧接着使用Http请求参数注解. 主要属性有:
required: 是否为必传参数
value: 参数简短说明
@ApiResponse
描述一个操作的可能返回结果. 当RESTful请求发生时,这个注解可用于描述所有可能的成功与错误码.可以使用也可以不使用这个注解去描述操作返回类型. 但成功操作后的返回类型必须在 @ApiOperation中定义. 如果API具有不同的返回类型,那么需要分别定义返回值,并将返回类型进行关联. 但是Swagger不支持同一返回码,多种返回类型的注解. 这个注解必须被包含在 @ApiResponses中:
code: http请求返回码,参照标准Http Status Code Definitions
message: 更加易于理解的文本消息
response: 返回类型信息,必须使用完全限定类名,即类的完整路径
responseContainer: 如果返回值类型为容器类型,可以设置相应的值. 有效值 :List, Set, Map. 其它的值将会被忽略
@ApiResponses
注解 @ApiResponse的包装类,数组结构. 即使需要使用一个 @ApiResponse注解,也需要将 @ApiResponse注解包含在注解 @ApiResponses内
@ApiImplicitParam
对API的单一参数进行注解. 注解 @ApiParam需要同JAX-RS参数相绑定, 但这个 @ApiImplicitParam注解可以以统一的方式定义参数列表,这是在Servlet和非JAX-RS环境下唯一的方式参数定义方式. 注意这个注解 @ApiImplicitParam必须被包含在注解 @ApiImplicitParams之内,可以设置以下重要属性:
name: 参数名称
value: 参数简短描述
required: 是否为必传参数
dataType: 参数类型,可以为类名,也可以为基本类型,比如String,int,boolean等
paramType: 参数的请求类型,可选的值有path, query, body, header, from
@ApiImplicitParams
注解 @ApiImplicitParam的容器类,以数组方式存储
@ApiModel
提供对Swagger model额外信息的描述. 在标注 @ApiOperation注解的操作内,所有类将自动introspected. 利用这个注解可以做一些更详细的model结构说明. 主要属性值有:
value: model的别名,默认为类名
description: model的详细描述
@ApiModelProperty
对model属性的注解,主要属性值有:
value: 属性简短描述
example: 属性示例值
required: 是否为必须值
数据库版本控制-Liquibase,flyway
Liquibase
Liquibase基本概念
Liquibase是一个用于跟踪,管理和应用数据库变化的数据重构和迁移的开源工具,通过日志文件的形式记录数据库的变更,然后执行日志文件中的修改,将数据库更新或回滚到一致的状态
Liquibase的主要特点:
不依赖于特定的数据库,支持所有主流的数据库. 比如MySQL, PostgreSQL, Oracle, SQL Server, DB2等.这样在数据库的部署和升级环节可以帮助应用系统支持多数据库
提供数据库比较功能,比较结果保存在XML中,基于XML可以用Liquibase部署和升级数据库
支持多开发者的协作维护,以XML存储数据库变化,以author和id唯一标识一个changeSet, 支持数据库变化的合并
日志文件支持多种格式. 比如XML, YAML, JSON, SQL等
支持多种运行方式. 比如命令行, Spring集成, Maven插件, Gradle插件等
在数据库中保存数据库修改历史DatabaseChangeHistory, 在数据库升级时自动跳过已应用的变化
提供变化应用的回滚功能,可按时间,数量或标签tag回滚已经应用的变化
可生成html格式的数据库修改文档
日志文件changeLog
changeLog是Liquibase用来记录数据库变更的日志文件,一般放在classpath下,然后配置到执行路径中
changeLog支持多种格式, 主要有XML, JSON, YAML, SQL, 推荐使用XML格式
一个 < changeSet > 标签对应一个变更集, 由属性id, name, changelog的文件路径唯一标识组合而成
changelog在执行时不是按照id的顺序,而是按照changSet在changlog中出现的顺序
在执行changelog时 ,Liquibase会在数据库中新建2张表,写执行记录:databasechangelog - changelog的执行日志和databasechangeloglock - changelog锁日志
在执行changelog中的changeSet时,会首先查看databasechangelog表,如果已经执行过,则会跳过,除非changeSet的runAlways属性为true, 如果没有执行过,则执行并记录changelog日志
changelog中的一个changeSet对应一个事务,在changeSet执行完后commit, 如果出现错误就会rollback
常用标签及命令
changeSet标签
< changeSet > 标签的主要属性有:
runAlways: 即使执行过,仍然每次都要执行
由于databasechangelog中还记录了changeSet的MD5校验值MD5SUM, 如果changeSet的id和name没变,而内容变化.则MD5值变化,这样即使runAlways的值为true, 也会导致执行失败报错.
这时应该使用runOnChange属性
runOnChange: 第一次的时候以及当changeSet发生变化的时候执行,不受MD5校验值的约束
runInTransaction: 是否作为一个事务执行,默认为true.
< changeSet > 有一个 < rollback > 子标签,用来定义回滚语句:
对于create table, rename column, add column等 ,Liquibase会自动生成对应的rollback语句
对于drop table, insert data等需要显式定义rollback语句
include标签
<?xml version="1.0" encoding="utf-8"?><databaseChangeLog
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<include file="logset-20160408/0001_authorization_init.sql" relativeToChangelogFile="true"/></databaseChangeLog>
includeAll标签
<includeAll path="com/example/changelogs/"/>
diff命令
java -jar liquibase.jar --driver=com.mysql.jdbc.Driver \
--classpath=./mysql-connector-java-5.1.29.jar \
--url=jdbc:mysql://127.0.0.1:3306/test \
--username=root --password=passwd \
diff \
--referenceUrl=jdbc:mysql://127.0.0.1:3306/authorization \
--referenceUsername=root --referencePassword=passwd
generateChangeLog
liquibase --driver=com.mysql.jdbc.Driver \
- classpath=./mysql-connector-java-5.1.29.jar \
- changeLogFile=liquibase/db.changeLog.xml \
--url=jdbc:mysql://127.0.0.1:3306/test \
--username=root
--password=root
generateChangeLog
generateChangeLog不支持存储过程,函数以及触发器
Liquibase使用示例
# Liquibase配置liquibase=true# changelog默认路径liquibase.change-log=classpath:/db/changelog/sqlData.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-2.0.xsd">
<changeSet author="chova" id="sql-01">
<sqlFile path="classpath:db/changelog/sqlfile/init.sql" encoding="UTF-8" />
<sqlFile path="classpath:db/changelog/sqlfile/users.sql" encoding="UTF-8" />
</changeSet>
<changeSet author="chova" id="sql-02">
<sqlFile path="classpath:db/changelog/sqlfile/users2.sql" encoding="UTF-8" />
</changeSet>
</databaseChangeLog>
CREATE TABLE usersTest(
user_id varchar2(14) DEFAULT '' NOT NULL,
user_name varchar2(128) DEFAULT '' NOT NULL)STORAGE(FREELISTS 20 FREELIST GROUPS 2) NOLOGGING TABLESPACE USER_DATA;insert into usersTest(user_id,user_name) values ('0','test');
<build>
<plugins>
<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<propertyFile>src/main/resources/liquibase.properties</propertyFile>
<propertyFileWillOverride>true</propertyFileWillOverride>
<!--生成文件的路径-->
<outputChangeLogFile>src/main/resources/changelog_dev.xml</outputChangeLogFile>
</configuration>
</plugin>
</plugins></build>
changeLogFile=src/main/resources/db/changelog/sqlData.xmldriver=oracle.jdbc.driver.OracleDriverurl=jdbc:oracle:thin:@chovausername=chovapassword=123456verbose=true# 生成文件的路径outputChangeLogFile=src/main/resources/changelog.xml
然后执行 [ mvn liquibase:generateChangeLog ] 命令,就是生成changelog.xml文件
liquibase:update
mnv liquibase:update
liquibase:rollback
rollbackCount示例:
rollbackDate示例: 需要注意日期格式,必须匹配当前平台执行DateFormat.getDateInstance() 得到的格式,比如 MMM d, yyyy
rollbackTag示例: 使用tag标识,需要先打tag, 然后rollback到tag
rollbackCount: 表示rollback的changeSet的个数
rollbackDate: 表示rollback到指定日期
rollbackTag: 表示rollback到指定的tag, 需要使用liquibase在具体的时间点上打上tag
mvn liquibase:rollback -Dliquibase.rollbackCount=3
mvn liquibase:rollback -Dliquibase.rollbackDate="Apr 10, 2020"
mvn liquibase:tag -Dliquibase.tag=tag20200410
mvn liquibase:rollback -Dliquibase.rollbackTag=tag20200410
flyway
flyway基本概念
flyway是一款数据库版本控制管理工具,支持数据库版本自动升级,不仅支持Command Line和Java API, 同时也支持Build构建工具和SpringBoot, 也可以在分布式环境下安全可靠地升级数据库,同时也支持失败恢复
flyway是一款数据库迁移 (migration) 工具,也就是在部署应用的时候,执行数据库脚本的应用,支持SQL和Java两种类型的脚本,可以将这些脚本打包到应用程序中,在应用程序启动时,由flyway来管理这些脚本的执行,这些脚本在flyway中叫作migration
开发人员将程序打包
应用部署人员拿到应用部署包,备份,替换,完成应用程序升级.期间flyway自动执行升级,备份脚本
开发人员将程序应用打包,按顺序汇总并整理数据库升级脚本
DBA拿到数据库升级脚本检查,备份,执行,以完成数据库升级
应用部署人员拿到应用部署包,备份,替换,完成应用程序升级
没有使用flyway时部署应用的流程:
引入flyway时部署应用的流程:
flyway的核心: MetaData表 - 用于记录所有版本演化和状态
flyway首次启动会创建默认名为SCHMA_VERSION表,保存了版本,描述和要执行的SQL脚本
flyway主要特性
普通SQL: 纯SQL脚本,包括占位符替换,没有专有的XML格式
无限制: 可以通过Java代码实现高级数据操作
零依赖: 只需运行在Java 6以上版本及数据库所需的JDBC驱动
约定大于配置: 数据库迁移时,自动查找系统文件和类路径中的SQL文件或Java类
高可靠性: 在集群环境下进行数据库的升级是安全可靠的
云支持: 完全支持Microsoft SQL Azure, Google Cloud SQL & App Engine, Heroku Postgres和Amazon RDS
自动迁移: 使用flyway提供的API, 可以让应用启动和数据库迁移同时工作
快速失败: 损坏的数据库或失败的迁移可以防止应用程序启动
数据库清理: 在一个数据库中删除所有的表,视图,触发器. 而不是删除数据库本身
SQL脚本
V1_INIT_DATABASE.sql
flyway.sql-migration-prefix=指定前缀
flyway工作原理
flyway对数据库进行版本管理主要由Metadata表和6种命令 : Migrate, Clean, Info, Validate, Undo, Baseline, Repair完成
在一个空数据库上部署集成flyway应用:
应用程序启动时 ,flyway在这个数据库中创建一张表,用于记录migration的执行情况,表名默认为:schema_version:
然后 ,flyway根据表中的记录决定是否执行应用程序包中提供的migration:
最后,将执行结果写入schema_version中并校验执行结果:
下次版本迭代时,提供新的migration, 会根据schema_version的记录执行新的migration:
flyway核心
Metadata Table
列名
类型
是否为null
键值
默认值
|
|
|
|
|
version_rank | int(11) | 否 | MUL | NULL |
installed_rank | int(11) | 否 | MUL | NULL |
version | varchar(50) | 否 | PRI | NULL |
description | varchar(200) | 否 |
| NULL |
type | varchar(20) | 否 |
| NULL |
script | varchar(1000) | 否 |
| NULL |
checksum | int(11) | 是 |
| NULL |
installed_by | varchar(100) | 否 |
| NULL |
installed_on | timestamp | 否 |
| CURRENT_TIMESTAMP |
execution_time | int(11) | 否 |
| NULL |
success | tinyint(1) | 否 | MUL | NULL |
Migration
prefix: 前缀标识.可以配置,默认情况下: V - Versioned, R - Repeatable
version: 标识版本号. 由一个或多个数字构成,数字之间的分隔符可以使用点或者下划线
separator: 用于分割标识版本号和描述信息. 可配置,默认情况下是两个下划线 _ _
description: 描述信息. 文字之间可以用下划线或空格分割
suffix: 后续标识. 可配置,默认为 .sql
确保版本号唯一 ,flyway按照版本号顺序执行 . repeatable没有版本号,因为repeatable migration会在内容改变时重复执行
默认情况下 ,flyway会将单个migration放在一个事务里执行,也可以通过配置将所有migration放在同一个事务里执行
public class V1_2_Another_user implements JdbcMigration { public void migrate(Connection connection) throws Exception {
PreparedStatement statement = connection.prepareStatement("INSERT INTO test_user (name) VALUES ("Oxford")"); try {
statement.execute();
} finally {
statement.close();
}
}
}
// 单行命令CREATE TABLE user (name VARCHAR(25) NOT NULL, PRIMARY KEY(name));
// 多行命令 -- Placeholder
INSERT INTO ${tableName} (name) VALUES ("oxford");
Callbacks
Name
Execution
|
|
beforeMigrate | Before Migrate runs |
beforeEachMigrate | Before every single migration during Migrate |
afterEachMigrate | After every single successful migration during Migrate |
afterEachMigrateError | After every single failed migration during Migrate |
afterMigrate | After successful Migrate runs |
afterMigrateError | After failed Migrate runs |
beforeClean | Before clean runs |
afterClean | After successful Clean runs |
afterCleanError | After failed Clean runs |
beforeInfo | Before Info runs |
afterInfo | After successful Info runs |
afterInfoError | After failed Info runs |
beforeValidate | Before Validate runs |
afterValidate | After successful Validate runs |
afterValidateError | After failed Validate runs |
beforeBaseline | Before Baseline runs |
afterBaseline | After successful Baseline runs |
afterBaselineError | After failed Baseline runs |
beforeRepair | BeforeRepair |
afterRepair | After successful Repair runs |
afterRepairError | After failed Repair runs |
flyway中6种命令
Migrate:
将数据库迁移到最新版本,是flyway工作流的核心功能.
flyway在Migrate时会检查元数据Metadata表.如果不存在会创建Metadata表,Metadata表主要用于记录版本变更历史以及Checksum之类
在Migrate时会扫描指定文件系统或classpath下的数据库的版本脚本Migrations, 并且会逐一比对Metadata表中已经存在的版本记录,如果未应用的Migrations,flyway会获取这些Migrations并按次序Apply到数据库中,否则不会做任何事情
通常会在应用程序启动时默认执行Migrate操作,从而避免程序和数据库的不一致
Clean:
来清除掉对应数据库的Schema的所有对象 .flyway不是删除整个数据库,而是清除所有表结构,视图,存储过程,函数以及所有相关的数据
通常在开发和测试阶段使用,能够快速有效地更新和重新生成数据库表结构.但是不应该在production的数据库使用
Info:
打印所有Migrations的详细和状态信息,是通过Metadata表和Migrations完成的
能够快速定位当前数据库版本,以及查看执行成功和失败的Migrations
Validate:
验证已经Apply的Migrations是否有变更 ,flyway是默认开启验证的
操作原理是对比Metadata表与本地Migration的Checksum值,如果相同则验证通过,否则验证失败,从而可以防止对已经Apply到数据库的本地Migrations的无意修改
Baseline:
针对已经存在Schema结构的数据库的一种解决方案
实现在非空数据库中新建Metadata表,并将Migrations应用到该数据库
可以应用到特定的版本,这样在已有表结构的数据库中也可以实现添加Metadata表,从而利用flyway进行新的Migrations的管理
Repair:
移除失败的Migration记录,这个问题针对不支持DDL事务的数据库
重新调整已经应用的Migrations的Checksums的值. 比如,某个Migration已经被应用,但本地进行了修改,又期望重新应用并调整Checksum值. 不建议对数据库进行本地修改
修复Metadata表,这个操作在Metadata表表现错误时很有用
通常有两种用途:
flyway的使用
正确创建Migrations
Migrations: flyway在更新数据库时使用的版本脚本
一个基于sql的Migration命名为V1_ _init_tables.sql, 内容即为创建所有表的sql语句
flyway也支持基于Java的Migration
flyway加载Migrations的默认Locations为classpath:db/migration, 也可以指定filesystem:/project/folder. Migrations的加载是在运行时自动递归执行的
除了指定的Locations外,flyway需要遵从命名格式对Migrations进行扫描,主要分为两类:
Repeatable是指可重复加载的Migrations, 其中每一次更新都会更新Checksum值,然后都会被重新加载,并不用于版本升级. 对于管理不稳定的数据库对象的更新时非常有用
Repeatable的Migrations总是在Versioned之后按顺序执行,开发者需要维护脚本并确保可以重复执行,通常会在sql语句中使用CREATE OR REPLACE来保证可重复执行
Versioned类型是常用的Migration类型
用于版本升级,每一个版本都有一个唯一的标识并且只能被应用一次. 并且不能再修改已经加载过的Migrations, 因为Metadata表会记录Checksum值
其中的version标识版本号,由一个或者多个数字构成,数字之间的分隔符可以采用点或者下划线,在运行时下划线也是被替换成点了. 每一部分的前导零都会被省略
Versioned migrations:
Repeatable migrations:
flyway数据库
# MySQLflyway.url=jdbc:mysql://localhost:3306/db?serverTimezone=UTC&useSSL=true# H2flyway.url=jdbc:h2:./.tmp/db# Hsqlflyway.url=jdbc:hsqldb:hsql//localhost:1476/db# PostgreSQLflyway.url=jdbc:postgresql://localhost:5432/postgres?currentSchema=schema
flyway命令行
flyway命令行工具支持直接在命令行中运行Migrate,Clean,Info,Validate,Baseline和Repair这6种命令
flyway会依次搜索以下配置文件:
/conf/flyway.conf
/flyway.conf
后面的配置会覆盖前面的配置
SpringBoot集成flyway
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>5.0.3</version></dependency><plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<version>5.0.3</version></plugin>
server.port=8080spring.datasource.url=jdbc:mysql://127.0.0.1:3306/dbspring.datasource.username=rootspring.datasource.password=123456spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
use db;CREATE TABLE person ( id int(1) NOT NULL AUTO_INCREMENT,
firstname varchar(100) NOT NULL,
lastname varchar(100) NOT NULL,
dateofbirth DATE DEFAULT NULL,
placeofbirth varchar(100) NOT NULL, PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8insert into person (firstname,lastname,dateofbirth,placeofbirth) values ('oxford','Eng',STR_TO_DATE('02/10/1997', '%m/%d/%Y'),'China');insert into person (firstname,lastname,dateofbirth,placeofbirth) values ('oxfordd','Engg',STR_TO_DATE('02/10/1995', '%m/%d/%Y'),'China');
启动springboot项目:
查看数据库:
默认情况下,生成flyway-schema-history表
如果需要指定schema表的命名,可以配置属性 : flyway.tableflyway
flyway配置
属性名
默认值
描述
|
|
|
baseline-description | / | 对执行迁移时基准版本的描述 |
baseline-on-migrate | false | 当迁移发现目标schema非空,而且带有没有元数据的表时,是否自动执行基准迁移 |
baseline-version | 1 | 开始执行基准迁移时对现有的schema的版本设置标签 |
check-location | false | 检查迁移脚本的位置是否存在 |
clean-on-validation-error | false | 校验错误时是否自动调用clean操作清空数据 |
enabled | true | 是否开启flyway |
encoding | UTF-8 | 设置迁移时的编码 |
ignore-failed-future-migration | false | 当读取元数据时,是否忽略错误的迁移 |
init-sqls | / | 初始化连接完成时需要执行的SQL |
locations | db/migration | 迁移脚本的位置 |
out-of-order | false | 是否允许无序迁移 |
password | / | 目标数据库密码 |
placeholder-prefix | ${ | 设置每个placeholder的前缀 |
placeholder-suffix | } | 设置每个placeholder的后缀 |
placeholders.[placeholder name] | / | 设置placeholder的value |
placeholder-replacement | true | placeholders是否要被替换 |
schemas | 默认的schema | 设置flyway需要迁移的schema,大小写敏感 |
sql-migration-prefix | V | 迁移文件的前缀 |
sql-migration-separator | _ _ | 迁移脚本的文件名分隔符 |
sql-migration-suffix | .sql | 迁移脚本的后缀 |
tableflyway | schema_version | 使用的元数据表名 |
target | latest version | 迁移时使用的目标版本 |
url | 配置的主数据源 | 迁移时使用的JDBC URL |
user | / | 迁移数据库的用户名 |
validate-on-migrate | true | 迁移时是否校验 |
部署-Docker
Docker基本概念
Docker基础架构
Docker引擎
Docker引擎: Docker Engine
Docker守护进程: Docker daemons,也叫dockerd.
Docker引擎API: Docker Engine API
Docker客户端: docker
是一个持久化进程,用户管理容器
Docker守护进程会监听Docker引擎API的请求
用于与Docker守护进程交互使用的API
是一个RESTful API,不仅可以被Docker客户端调用,也可以被wget和curl等命令调用
是大部分用户与Docker交互的主要方式
用户通过客户端将命令发送给守护进程
命令遵循Docker Engine API
是一个服务端 - 客户端结构的应用
主要组成部分:
Docker注册中心
Docker注册中心: Docker registry,用于存储Docker镜像
Docker Hub: Docker的公共注册中心,默认情况下,Docker在这里寻找镜像.也可以自行构建私有的注册中心
Docker对象
Docker对象指的是 :Images,Containers,Networks, Volumes,Plugins等等
允许用户跨越不同的Docker守护进程的情况下增加容器
并将这些容器分为管理者(managers)和工作者(workers),来为swarm共同工作
镜像可运行的实例
容器可以通过API或者CLI(命令行)进行操作
一个只读模板,用于指示创建容器
镜像是分层构建的,定义这些层次的文件叫作Dockerfile
镜像: Images
容器: Containers
服务: Services
Docker扩展架构
Docker Compose
Docker Compose是用来定义和运行多个容器Docker应用程序的工具
通过Docker Compose, 可以使用YAML文件来配置应用程序所需要的所有服务,然后通过一个命令,就可以创建并启动所有服务
Docker Compose对应的命令为 : docker-compose
Swarm Mode
从Docker 1.12以后 ,swarm mode集成到Docker引擎中,可以使用Docker引擎API和CLI命令直接使用
Swarm Mode内置 k-v 存储功能,特点如下:
具有容错能力的去中心化设计
内置服务发现
负载均衡
路由网格
动态伸缩
滚动更新
安全传输
Swarm Mode的相关特性使得Docker本地的Swarm集群具备与Mesos.Kubernetes竞争的实力
cluster: 集群
swarm: 群
swarm mode是指Docker引擎内嵌的集群管理和编排功能
当初始化一个cluster中的swarm或者将节点加入一个swarm时 ,Docker引擎就会以swarm mode的形式运行
一个集群的Docker引擎以swarm mode形式运行
Swarm Mode原理:
managers: 管理者. 用于处理集群关系和委派
workers: 工作者. 用于执行swarm服务
比如,一个工作节点宕机后,那么Docker就会把这个节点的任务委派给另外一个节点
这里的任务task是指: 被swarm管理者管理的一个运行中的容器
当创建swarm服务时,可以增加各种额外的状态: 数量,网络,端口,存储资源等等
Docker会去维持用户需要的状态:
swarm中的Docker机器分为两类:
swarm相对于单独容器的优点:
修改swarm服务的配置后无需重启
Docker以swarm mode形式运行时,可以选择直接启动单独的容器
在swarm mode下,可以通过docker stack deploy使用Compose文件部署应用栈
swarm服务分为两种:
swarm管理员可以使用ingress负载均衡使服务可以被外部接触
swarm管理员会自动地给服务分配PublishedPort, 或者手动配置.
Swarm Mode有内部DNS组件,会为每个服务分配一个DNS条目 . swarm管理员使用internal load balancing去分发请求时,就是依靠的这个DNS组件
Swarm Mode功能是由swarmkit提供的,实现了Docker的编排层,使得swarm可以直接被Docker使用
文件格式
Dockerfile
Compose文件
Compose文件是一个YAML文件,定义了服务, 网络 和卷:
service: 服务. 定义各容器的配置,定义内容将以命令行参数的方式传给docker run命令
network: 网络. 定义各容器的配置,定义内容将以命令行参数的方式传给docker network create命令
volume: 卷. 定义各容器的配置,定义内容将以命令行参数的方式传给docker volume create命令
docker run命令中有一些选项,和Dockerfile文件中的指令效果是一样的: CMD, EXPOSE, VOLUME, ENV. 如果Dockerfile文件中已经使用了这些命令,那么这些指令就被视为默认参数,所以无需在Compose文件中再指定一次
Compose文件中可以使用Shell变量:
db:
image: "postgres:${POSTGRES_VERSION}"
网络
bridge
Docker中的网桥使用的软件形式的网桥
使用相同的网桥的容器连接进入该网络,非该网络的容器无法进入
Docker网桥驱动会自动地在Docker主机上安装规则,这些规则使得不同桥接网络之间不能直接通信
桥接经常用于:
网桥网络适用于容器运行在相同的Docker守护进程的主机上
不同Docker守护进程主机上的容器之间的通信需要依靠操作系统层次的路由,或者可以使用overlay网络进行代替
bridge: 网桥驱动
是Docker默认的网络驱动,接口名为docker0
当没有为容器指定一个网络时,Docker将使用这个驱动
可以通过daemon.json文件修改相关配置
自定义网桥可以通过 brtcl 命令进行配置
host
overlay
macvlan
macvlan:
允许赋予容器MAC地址
在该网络里,容器会被认为是物理设备
none
在该策略下,容器不使用任何网络
none常常用于连接自定义网络驱动的情况下
其它网络策略模式
数据管理
在默认情况下,Docker所有文件将会存储在容器里的可写的容器层container layer:
数据与容器共为一体: 随着容器的消失,数据也会消失. 很难与其它容器程序进行数据共享
容器的写入层与宿主机器紧紧耦合: 很难移动数据到其它容器
容器的写入层是通过存储驱动storage driver管理文件系统: 存储驱动会使用Linux内核的链合文件系统union filesystem进行挂载,相比较于直接操作宿主机器文件系统的数据卷,这个额外的抽象层会降低性能
容器有两种永久化存储方式:
volumes: 卷
bind mounts: 绑定挂载
Linux中可以使用tmpfs进行挂载, windows用户可以使用命名管道named pipe.
在容器中,不管使用哪种永久化存储,表现形式都是一样的
卷
卷: volumes.
Docker推荐使用卷进行持久化数据
卷可以支持卷驱动volume drivers: 该驱动允许用户将数据存储到远程主机或云服务商cloud provider或其它
没有名字的卷叫作匿名卷anonymous volume. 有名字的卷叫作命名卷named volume. 匿名卷没有明确的名字,当被初始化时,会被赋予一个随机名字
绑定挂载
绑定挂载: bind mounts
通过将宿主机器的路径挂载到容器里的这种方式,从而实现数据持续化,因此绑定挂载可将数据存储在宿主机器的文件系统中的任何地方
非Docker程序可以修改这些文件
绑定挂载在Docker早起就已经存在,与卷存储相比较,绑定挂载十分简单明了
在开发Docker应用时,应使用命名卷named volume代替绑定挂载,因为用户不能对绑定挂载进行Docker CLI命令操作
绑定挂载的使用场景:
tmpfs挂载
tmpfs挂载: tmpfs mounts
swarm服务通过tmpfs将secrets数据(密码,密钥,证书等)存储到swarm服务
仅仅存储于内存中,不操作宿主机器的文件系统.即不持久化于磁盘
用于存储一些非持久化状态,敏感数据
命名管道
命名管道: named pipes
在容器内运行第三方工具,并使用命名管道连接到Docker Engine API
通过pipe挂载的形式,使Docker主机和容器之间互相通讯
覆盖问题
当挂载空的卷至一个目录中,目录中你的内容会被复制于卷中,不会覆盖
如果挂载非空的卷或绑定挂载至一个目录中,那么该目录的内容将会被隐藏obscured,当卸载后内容将会恢复显示
日志
在Linux和Unix中,常见的I/O流分为三种:
STDIN: 输入
STDOUT: 正常输出
STDERR: 错误输出
默认配置下,Docker的日志所记载的是命令行的输出结果:
STDOUT : /dev/stdout
STDERR : /dev/stderr
也可以在宿主主机上查看容器的日志,使用命令可以查看容器日志的位置:
docker inspect --format='{{.LogPath}}' $INSTANCE_ID
持续集成-jenkins
jenkins基本概念
jenkins是一个开源的,提供友好操作页面的持续集成(CI)工具
jenkins主要用于持续,自动的构建或者测试软件项目,监控外部任务的运行
jenkins使用Java语言编写,可以在Tomcat等流行的servlet容器中运行,也可以独立运行
通常与版本管理工具SCM, 构建工具结合使用
常用的版本控制工具有SVN,GIT
常见的构建工具有Maven,Ant,Gradle
CI/CD
CI: Continuous integration, 持续集成
持续集成强调开发人员提交新代码之后,like进行构建,单元测试
根据测试结果,可以确定新代码和原有代码能否正确地合并在一起
CD: Continuous Delivery, 持续交付
比如在完成单元测试后,可以将代码部署到连接数据库的Staging环境中进行更多的测试
在持续集成的基础上,将集成后的代码部署到更贴近真实运行环境中,即类生产环境中
如果代码没有问题,可以继续手动部署到生产环境
jenkins使用配置
General
源码管理
源码管理用于配置代码的存放位置
Git: 支持主流的github和gitlab代码仓库
Repository URL: 仓库地址
Credentials: 凭证. 可以使用HTTP方式的用户名和密码,也可以是RSA文件.但是要通过后面的[ADD]按钮添加凭证
Branches to build: 构建分支. */master表示master分支,也可以设置为另外的分支
源码浏览器: 所使用的代码仓库管理工具,如github,gitlab
URL: 填入上方的仓库地址即可
Version: gitlab服务器版本
Subversion: 就是SVN
构建触发器
构建任务的触发器
触发远程构建(例如,使用脚本): 这个选项会提供一个接口,可以用来在代码层面触发构建
Build after other project are built: 在其它项目构建后构建
Build periodically: 周期性地构建.每隔一段时间进行构建
webhooks: 触发构建的地址,需要将这个地址配置到gitlab中
日程表: 类似linux cronttab书写格式. 下图表示每隔30分钟进行一次构建
Build when a change is pushed to Gitlab: 常用的构建触发器,当有代码push到gitlab代码仓库时就进行构建
Poll SCM: 这个功能需要与上面的这个功能配合使用. 当代码仓库发生变动时,jekins并不知道. 这时,需要配置这个选项,周期性地检查代码仓库是否发生变动
构建环境
构建环境: 构建之前的准备工作. 比如指定构建工具,这里使用Ant
With Ant: 选择这个选项,并指定Ant版本和JDK版本. 需要事先在jenkins服务器上安装这两个版本的工具,并且在jenkins全局工具中配置好
构建
点击下方的增加构建步骤:
这里有多种增加构建步骤的方式,在这里介绍Execute shell和Invoke Ant
Execute shell: 执行shell命令. 该工具是针对linux环境的,windows中对应的工具是 [Execute Windows batch command]. 在构建之前,需要执行一些命令: 比如压缩包的解压等等
Invoke Ant: Ant是一个Java项目构建工具,也可以用来构建PHP
Ant Version: 选择Ant版本. 这个Ant版本是安装在jenkins服务器上的版本,并且需要在jenkins[系统工具]中设置好
Targets: 需要执行的操作. 一行一个操作任务: 比如上图的build是构建,tar是打包
Build File: Ant构建的配置文件. 如果不指定,默认是在项目路径下的workspace目录中的build.xml
properties: 设定一些变量. 这些变量可以在build.l中被引用
Send files or execute commands over SSH: 发送文件到远程主机或者执行命令脚本
Name: SSH Server的名称. SSH Server可以在jenkins[系统设置]中配置
Source files: 需要发送给远程主机的源文件
Remove prefix: 移除前面的路径. 如果不设置这个参数,默认情况下远程主机会自动创建构建源source file包含的路径
Romote directory: 远程主机目录
Exec command: 在远程主机上执行的命令或者脚本
构建后操作
构建后操作: 对构建完成的项目完成一些后续操作:比如生成相应的代码测试报告
Publish Clover PHP Coverage Report: 发布代码覆盖率的xml格式的报告. 路径在build.xml中定义
Publish HTML reports: 发布代码覆盖率的HTML报告
Report Crap: 发布Crap报告
E-mail Notification: 邮件通知. 构建完成后发送邮件到指定的邮箱
配置完成后,点击[保存]
其它配置
SSH Server配置
Ant配置文件 - build.xml
配置Gitlab webhooks
在gitlab的project页面打开settings
打开web hooks
点击[ADD WEB HOOK] 来添加webhook
将之前的jenkins配置中的url添加到这里
添加完成后,点击 [TEST HOOK] 进行测试,如果显示SUCCESS则表示添加成功
配置phpunit.xml
phpunit.xml: 是phpunit工具用来单元测试所需要的配置文件
这个文件的名称是可以自定义的,只要在build.xml中配置好名字即可
默认情况下,如果使用phpunit.xml, 就不需要在build.xml中配置文件名
fileset dir: 指定单元测试文件所在路径.
include: 指定包含哪些文件,支持通配符
exclude: 指定不包含的文件
构建jenkins project
第一次配置好jenkins project后,会触发一次构建
此后,每当有commit提交到master分支(根据配置中的分支触发), 就会触发一次构建
也可以在project页面手动触发构建: 点击 [立即构建] 即可手动触发构建
构建结果说明
构建状态
Successful: 蓝色. 构建完成,并且是稳定的
Unstable: 黄色. 构建完成,但是是不稳定的
Failed: 红色. 构建失败
Disable: 灰色. 构建已禁用
构建稳定性
构建历史界面
jenkins权限管理
项目视图:
安装Role Strategy Plugin插件
安装Role Stratey Plugin后进入系统设置页面,按照如下配置后,点击 [保存] :
点击 [系统管理] -> [Manage and Assign Roles] 进入角色管理页面:
选择 [Manager Roles], 按照下图配置后点击 [保存]:
比如过滤TEST开头的jobs,要写成 : TEST.,而不是 TEST
job_read只加overall的read权限
job_create只加job的create权限
project roles中Pattern正则表达式和脚本里的是不一样的:
进入[系统设置] -> [Manage and Assign Roles] -> [Assign Roles] , 按照如下模板配置后,点击 [保存]
Anonymous必须变成用户,给job_create组和job_read组权限,否则将没有OverAll的read权限
project roles: 用于对应用户不同的权限
验证: 登录对应的用户权限后查看用户相关权限
视图通过正则表达式过滤job: 设置正则表达式为wechat.*,表示过滤所有以wechat开头的项目
设置后的效果如图:
自动化测试-TestNG
TestNG基本概念
TestNG是一个Java语言的开源测试框架,类似JUnit和NUnit,但是功能强大,更易于使用
TestNG的设计目标是为了覆盖更广泛的测试类别范围:
TestNG的主要功能:
测试可以运行在任意大的线程池中,并有多种运行策略可以选择: 所有方法都有自己的线程,或者每一个测试类一个线程等等
测试代码是否线程安全
TestNG的运行,既可以通过Ant的build.xml: 有或这没有一个测试套定义. 又可以通过带有可视化效果的IDE插件
不需要TestSuite类,测试包,测试组以及选择运行的测试. 都通过XML文件来定义和配置
支持注解
支持参数化和数据驱动测试: 使用@DataProvider或者XML配置
支持同一类的多个实例: @Factory
灵活的执行模式:
并发测试:
嵌入BeanShell可以获得更大的灵活性
默认使用JDK运行和相关日志功能,不需要额外增加依赖
应用服务器测试的依赖方法
分布式测试: 允许在从机上进行分布式测试
TestNG环境配置
配置好主机的Java环境,使用命令 java -version查看
在TestNG官网,下载TestNG对应系统下的jar文件
系统环境变量中添加指向jar文件的路径
在IDEA中安装TestNG
TestNG的基本用法
import org.junit.AfterClass;import org.junit.BeforeClass;import org.testng.annotations.Test;public class TestNGLearn1 { @BeforeClass
public void beforeClass() {
System.out.println("this is before class");
} @Test
public void TestNgLearn() {
System.out.println("this is TestNG test case");
} @AfterClass
public void afterClass() {
System.out.println("this is after class");
}
}
TestNG的基本注解
注解
描述
|
|
@BeforeSuit | 注解方法只运行一次,在此套件中所有测试之前运行 |
@AfterSuite | 注解方法只运行一次,在此套件中所有测试之后运行 |
@BeforeClass | 注解方法只运行一次,在当前类中所有方法调用之前运行 |
@AfterClass | 注解方法只运行一次,在当前类中所有方法调用之后运行 |
@BeforeTest | 只运行一次,在所有的测试方法执行之前运行 |
@AfterTest | 只运行一次,在所有的测试方法执行之后运行 |
@BeforeGroups | 组的列表,配置方法之前运行. 此方法是保证在运行属于任何这些组的第一个测试,该方法将被调用 |
@AfterGroups | 组的名单,配置方法之后运行. 此方法是保证运行属于任何这些组的最后一个测试后不久,,该方法将被调用 |
@BeforeMethod | 在每一个@test测试方法运行之前运行 比如:在执行完测试用例后要重置数据才能执行第二条测试用例时,可以使用这种注解方式 |
@AfterMethod | 在每一个@test测试方法运行之后运行 |
@DataProvider | 标志一个方法,提供数据的一个测试方法 注解的方法必须返回一个Object[][],其中每个对象的[]的测试方法的参数列表可以分配 如果有@Test方法,想要使用从这个DataProvider中接收的数据,需要使用一个dataProvider名称等于这个注解的名称 |
@Factory | 作为一个工厂,返回TestNG的测试类对象中被用于标记的方法 该方法必须返回Object[] |
@Listeners | 定义一个测试类的监听器 |
@Parameters | 定义如何将参数传递给@Test方法 |
@Test | 标记一个类或者方法作为测试的一部分 |
testng.xml
属性
描述
|
|
name | 套件suite的名称,这个名称会出现在测试报告中 |
junit | 是否以junit模式运行 |
verbose | 设置在控制台中的输出方式. 这个设置不影响html版本的测试报告 |
parallel | 是否使用多线程进行测试,可以加速测试 |
configfailurepolicy | 是否在运行失败了一次之后继续尝试或者跳过 |
thread-count | 如果设置了parallel,可以设置线程数 |
annotations | 如果有javadoc就在javadoc中寻找,没有就使用jdk5的注释 |
time-out | 在终止method(parallel="methods")或者test(parallel="tests")之前设置以毫秒为单位的等待时间 |
skipfailedinvocationcounts | 是否跳过失败的调用 |
data-provider-thread-count | 提供一个线程池的范围来使用parallel data |
object-factory | 用来实例化测试对象的类,继承自IObjectFactory类 |
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" ><suite name="Suite" parallel="tests" thread-count="5">
<test name="Test" preserve-order="true" verbose="2">
<parameter name="userName" value="15952031403"></parameter>
<parameter name="originPwd" value="c12345"></parameter>
<classes>
<class name="com.oxford.testng.RegisterTest">
</class>
</classes>
</test>
<test name="Test1" preserve-order="true">
<classes>
<class name="com.oxford.testng.Test2">
</class>
</classes>
</test>
<test name="Test2" preserve-order="true">
<classes>
<class name="com.oxford.testng.Test3">
</class>
</classes>
</test></suite>
在suite中,同时使用parallel和thread-count:
preserve-order: 当设置为true时,节点下的方法按顺序执行
verbose: 表示记录日志的级别,在0 - 10之间取值
< parameter name="userName", value="15952031403" > : 给测试代码传递键值对参数,在测试类中通过注解 @Parameter({"userName"}) 获取
参数化测试
当测试逻辑一样,只是参数不一样时,可以采用数据驱动测试机制,避免重复代码
TestNG通过 @DataProvider实现数据驱动
使用@DataProvider做数据驱动:
通过@DataProvider读取XML文件中的数据
然后测试方法只要标示获取数据来源的DataProvider
对应的DataProvider就会将读取的数据传递给该test方法
数据源文件可以是EXCEL,XML,甚至可以是TXT文本
比如读取xml文件:
构建XML数据文件
<?xml version="1.0" encoding="UTF-8"?><data>
<login>
<username>user1</username>
<password>123456</password>
</login>
<login>
<username>user2</username>
<password>345678</password>
</login></data>
读取XML文件
import java.io.File;import java.util.ArrayList;import java.util.HashMap;import java.util.Iterator;import java.util.List;import java.util.Map;import org.dom4j.Document;import org.dom4j.DocumentException;import org.dom4j.Element;import org.dom4j.io.SAXReader;public class ParseXml { /**
* 利用Dom4j解析xml文件,返回list
* @param xmlFileName
* @return
*/
public static List parse3Xml(String xmlFileName){
File inputXml = new File(xmlFileName);
List list= new ArrayList();
int count = 1;
SAXReader saxReader = new SAXReader(); try {
Document document = saxReader.read(inputXml);
Element items = document.getRootElement(); for (Iterator i = items.elementIterator(); i.hasNext();) {
Element item = (Element) i.next();
Map map = new HashMap();
Map tempMap = new HashMap(); for (Iterator j = item.elementIterator(); j.hasNext();) {
Element node = (Element) j.next();
tempMap.put(node.getName(), node.getText());
}
map.put(item.getName(), tempMap);
list.add(map);
}
} catch (DocumentException e) {
System.out.println(e.getMessage());
}
System.out.println(list.size()); return list;
}
}
DataProvider类
import java.lang.reflect.Method;import java.util.ArrayList;import java.util.List;import java.util.Map;import org.testng.Assert;import org.testng.annotations.DataProvider;public class GenerateData { public static List list = new ArrayList();
@DataProvider(name = "dataProvider")
public static Object[][] dataProvider(Method method){
list = ParseXml.parse3Xml("absolute path of xml file");
List<Map<String, String>> result = new ArrayList<Map<String, String>>();
for (int i = 0; i < list.size(); i++) {
Map m = (Map) list.get(i);
if(m.containsKey(method.getName())){
Map<String, String> dm = (Map<String, String>) m.get(method.getName());
result.add(dm);
}
}
if(result.size() > 0){
Object[][] files = new Object[result.size()][]; for(int i=0; i<result.size(); i++){
files[i] = new Object[]{result.get(i)};
}
return files;
}else {
Assert.assertTrue(result.size()!=0,list+" is null, can not find"+method.getName() ); return null;
}
}
}
在test方法中引用DataProvider
public class LoginTest { @Test(dataProvider="dataProvider", dataProviderClass= GenerateData.class)
public void login(Map<String, String> param) throws InterruptedException{
List<WebElement> edits = findElementsByClassName(AndroidClassName.EDITTEXT);
edits.get(0).sendkeys(param.get("username"));
edits.get(1).sendkeys(param.get("password"));
}
}
TestNG重写监听类
package com.oxford.listener;import java.text.SimpleDateFormat;import java.util.ArrayList;import java.util.Date;import java.util.List;import org.testng.ITestContext;import org.testng.ITestResult;import org.testng.TestListenerAdapter;import com.google.gson.Gson;import com.google.gson.JsonObject;import com.unionpay.base.BaseTest;import com.unionpay.constants.CapabilitiesBean;import com.unionpay.constants.CaseCountBean;import com.unionpay.constants.ResultBean;import com.unionpay.util.Assertion;import com.unionpay.util.PostService;import com.unionpay.util.ReadCapabilitiesUtil;/**
* 带有post请求的testng监听
* @author lichen2
*/public class TestNGListenerWithPost extends TestListenerAdapter{
//接收每个case结果的接口
private String caseUrl;
//接收整个test运行数据的接口
private String countUrl;
//接收test运行状态的接口
private String statusUrl;
private JsonObject caseResultJson = new JsonObject();
private JsonObject caseCountJson = new JsonObject();
private Gson gson = new Gson();
private ResultBean result = new ResultBean();
private CaseCountBean caseCount = new CaseCountBean();
private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private CapabilitiesBean capabilitiesBean = ReadCapabilitiesUtil.readCapabilities("setting.json");
private String testStartTime;
private String testEndTime;
private String runId;
//testng初始化
@Override
public void onStart(ITestContext testContext) { super.onStart(testContext);
String serverUrl = capabilitiesBean.getServerurl();
caseUrl = "http://"+serverUrl+"/api/testcaseResult";
countUrl = "http://"+serverUrl+"/api/testcaseCount";
statusUrl = "http://"+serverUrl+"/api/testStatus";
runId = capabilitiesBean.getRunid();
result.setRunId(runId);
caseCount.setRunId(runId);
}
//case开始
@Override
public void onTestStart(ITestResult tr) {
Assertion.flag = true;
Assertion.errors.clear();
sendStatus("运行中");
result.setStartTime(format.format(new Date()));
}
//case成功执行
@Override
public void onTestSuccess(ITestResult tr) { super.onTestSuccess(tr);
sendResult(tr);
takeScreenShot(tr);
} //case执行失败
@Override
public void onTestFailure(ITestResult tr) { super.onTestFailure(tr);
sendResult(tr); try {
takeScreenShot(tr);
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} this.handleAssertion(tr);
} //case被跳过
@Override
public void onTestSkipped(ITestResult tr) { super.onTestSkipped(tr);
takeScreenShot(tr);
sendResult(tr); this.handleAssertion(tr);
} //所有case执行完成
@Override
public void onFinish(ITestContext testContext) { super.onFinish(testContext);
sendStatus("正在生成报告");
sendFinishData(testContext);
}
/**
* 发送case测试结果
* @param tr
*/
public void sendResult(ITestResult tr){
result.setTestcaseName(tr.getName());
result.setEndTime(format.format(new Date())); float tmpDuration = (float)(tr.getEndMillis() - tr.getStartMillis());
result.setDuration(tmpDuration / 1000);
switch (tr.getStatus()) { case 1:
result.setTestResult("SUCCESS"); break; case 2:
result.setTestResult("FAILURE"); break; case 3:
result.setTestResult("SKIP"); break; case 4:
result.setTestResult("SUCCESS_PERCENTAGE_FAILURE"); break; case 16:
result.setTestResult("STARTED"); break; default: break;
}
caseResultJson.addProperty("result", gson.toJson(result));
PostService.sendPost(caseUrl, caseResultJson.toString());
}
/**
* 通知test完成
* @param testContext
*/
public void sendFinishData(ITestContext tc){
testStartTime = format.format(tc.getStartDate());
testEndTime = format.format(tc.getEndDate()); long duration = getDurationByDate(tc.getStartDate(), tc.getEndDate());
caseCount.setTestStartTime(testStartTime);
caseCount.setTestEndTime(testEndTime);
caseCount.setTestDuration(duration);
caseCount.setTestSuccess(tc.getPassedTests().size());
caseCount.setTestFail(tc.getFailedTests().size());
caseCount.setTestSkip(tc.getSkippedTests().size());
caseCountJson.addProperty("count", gson.toJson(caseCount));
PostService.sendPost(countUrl, caseCountJson.toString());
}
/**
* 通知test运行状态
*/
public void sendStatus(String status){
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("runId", runId);
jsonObject.addProperty("status", status);
JsonObject sendJson = new JsonObject();
sendJson.addProperty("status", jsonObject.toString());
PostService.sendPost(statusUrl, sendJson.toString());
}
//计算date间的时差(s)
public long getDurationByDate(Date start, Date end){ long duration = end.getTime() - start.getTime(); return duration / 1000;
} //截图
private void takeScreenShot(ITestResult tr) {
BaseTest b = (BaseTest) tr.getInstance();
b.takeScreenShot(tr);
}
}
package com.oxford.base;import org.testng.ITestResult;import com.unionpay.listener.TestNGListenerWithPost;@Listeners(TestNGListenerWithPost.class)public abstract class BaseTest { public AndroidDriver<WebElement> driver; public BaseTest() {
driver = DriverFactory.getDriverByJson();
} /**
* 截屏并保存到本地
* @param tr
*/
public void takeScreenShot(ITestResult tr) {
String fileName = tr.getName() + ".jpg";
File dir = new File("target/snapshot"); if (!dir.exists()) {
dir.mkdirs();
}
String filePath = dir.getAbsolutePath() + "/" + fileName; if (driver != null) { try {
File scrFile = driver.getScreenshotAs(OutputType.FILE);
FileUtils.copyFile(scrFile, new File(filePath));
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
本文作者:返回主页攻城狮Chova 来源:博客园
CIO之家 www.ciozj.com 微信公众号:imciow