微服务架构技术栈:程序员必须掌握的微服务架构框架详细解析

2022-01-22 09:34:45 浏览数 (1)

主要技术

  • 基础框架: springboot
  • 微服务架构: dubbo,springboot cloud
  • ORM框架: mybatis plus
  • 数据库连接池: Alibaba Druid
  • 网关(统一对外接口 ): zuul
  • 缓存: redis
  • 注册中心: zookeeper,eureka
  • 消息队列:
  • 作业调度框架: Quartz
  • 分布式文件系统:
  • 接口测试框架: Swagger2
  • 数据库版本控制: Liquibase (flyway)
  • 部署: docker
  • 持续集成: jenkins
  • 自动化测试: testNG

ORM框架-Mybatis Plus

MyBatis Plus是在 MyBatis 的基础上只做增强不做改变,可以简化开发,提高效率.

Mybatis Plus核心功能

  • 支持通用的 CRUD,代码生成器与条件构造器
  • 通用CRUD: 定义好Mapper接口后,只需要继承 BaseMapper接口即可获得通用的增删改查功能,无需编写任何接口方法与配置文件
  • 条件构造器: 通过EntityWrapper(实体包装类),可以用于拼接SQL语句,并且支持排序,分组查询等复杂的 SQL
  • 代码生成器: 支持一系列的策略配置与全局配置,比 MyBatis 的代码生成更好用 BaseMapper接口中通用的 CRUD 方法:

MyBatis Plus与SpringBoot集成

  • 数据库USER
代码语言:javascript复制
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;
  • pom.xml依赖
代码语言:javascript复制
 <!--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>
  • spring-mybatis.xml配置文件 也可以直接使用@Bean的方式进行或者通过application配置文件进行
代码语言:javascript复制
<?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>
  • 编写启动类,应用启动时自动加载配置xml文件
代码语言:javascript复制
@Configuration
@ImportResource(locations = {"classpath:/mybatis/spring-mybatis.xml"})
//@MapperScan("cn.lqdev.learning.springboot.chapter9.biz.dao")
//@EnableTransactionManagement
public class MybatisPlusConfig {
}

MyBatis Plus集成Spring

  • 数据表结构
代码语言:javascript复制
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;
  • pom.xml
代码语言:javascript复制
    <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>
  • MyBatis全局配置文件mybatis-config.xml
代码语言:javascript复制
<?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 />
  • 数据源db.properties
代码语言:javascript复制
jdbc.url=jdbc:mysql://localhost:3306/mp
jdbc.username=mp
jdbc.password=mp
  • Spring 配置文件applicationContext.xml
代码语言:javascript复制
    <!-- 数据源 -->
    <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使用示例

  • 实体类Employee
代码语言:javascript复制
@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() 方法
  • mapper接口
代码语言:javascript复制
/**
* 不定义任何接口方法
*/
public interface EmployeeMapper extends BaseMapper<Employee> {}
  • 在测试类中生成测试的mapper对象
代码语言:javascript复制
    private ApplicationContext context = 
            new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
            
    private EmployeeMapper employeeMapper = 
            context.getBean("employeeMapper", EmployeeMapper.class);
  • 查询:
代码语言:javascript复制
    @Test
    public void getEmpByIdTest() {
        Employee employee = employeeMapper.selectById(1);
        
        System.out.println(employee);
    }
  • 分页查询:
代码语言:javascript复制
    @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);
    }
  • 条件构造器:
代码语言:javascript复制
    @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使用场景

代码生成器
  • 代码生成器依赖velocity模版引擎,引入依赖
代码语言:javascript复制
<dependency>
	<groupId>org.apache.velocity</groupId>
	<artifactId>velocity-engine-core</artifactId>
	<version>2.0</version>
	<scope>test</scope>
</dependency>
  • 代码生成器类MysqlGenerator:
代码语言:javascript复制
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
  • 通用CRUD测试类GeneralTest:
代码语言:javascript复制
@RunWith(SpringRunner.class)
//SpringBootTest 是springboot 用于测试的注解,可指定启动类或者测试环境等,这里直接默认。
@SpringBootTest 
@Slf4j
public 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());
    }
}
  • MyBatis Plus定义的数据库操作方法

对于通用代码如何注入的,可查看com.baomidou.mybatisplus.mapper.AutoSqlInjector类,这个就是注入通用的CURD方法的类.

条件构造器

条件构造器主要提供了实体包装器,用于处理SQL语句拼接,排序,实体参数查询:使用的是数据库字段,不是Java属性

  • sql条件拼接: SQL条件拼接测试类ConditionTest
代码语言:javascript复制
@RunWith(SpringRunner.class)
//SpringBootTest 是springboot 用于测试的注解,可指定启动类或者测试环境等,这里直接默认。
@SpringBootTest 
@Slf4j
public 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

  • 自定义SQL使用条件构造器: UserDao.java加入接口方法:
代码语言:javascript复制
/**
     * 
     * @param rowBounds 分页对象 直接传入page即可
     * @param wrapper 条件构造器
     * @return
     */
    List<User> selectUserWrapper(RowBounds rowBounds, @Param("ew") Wrapper<User> wrapper);

UserMapper.xml加入对应的xml节点:

代码语言:javascript复制
    <!-- 条件构造器形式 -->
    <select id="selectUserWrapper" resultType="user">
        SELECT
        <include refid="Base_Column_List" />
        FROM USER
        <where>
            ${ew.sqlSegment}
        </where>
    </select>

自定义SQL使用条件构造器测试类:

代码语言:javascript复制
@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结束");
    }
  • xml形式使用wrapper: UserDao.java:
代码语言:javascript复制
/**
     * 
     * @param rowBounds 分页对象 直接传入page即可
     * @param wrapper 条件构造器
     * @return
     */
    List<User> selectUserWrapper(RowBounds rowBounds, @Param("ew") Wrapper<User> wrapper);

UserMapper.xml:

代码语言:javascript复制
    <!-- 条件构造器形式 -->
    <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接口,有两种方式:

  • 注解形式:
代码语言:javascript复制
@Select("SELECT * FROM USER WHERE CODE = #{userCode}")
    List<User> selectUserCustomParamsByAnno(@Param("userCode")String userCode);
  • xml形式:
代码语言:javascript复制
List<User> selectUserCustomParamsByXml(@Param("userCode")String userCode);

UserMapper.xml新增一个节点:

代码语言:javascript复制
    <!-- 由于设置了别名: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:

代码语言:javascript复制
@RunWith(SpringRunner.class)
//SpringBootTest 是springboot 用于测试的注解,可指定启动类或者测试环境等,这里直接默认。
@SpringBootTest 
@Slf4j
public 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文件:

代码语言:javascript复制
  <!--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的插件机制使用只需要注册即可

  • mybatis-config.xml
代码语言:javascript复制
    <plugins>
      <!-- SQL 执行性能分析,开发环境使用,线上不推荐。 -->
      <plugin interceptor="com.baomidou.mybatisplus.plugins.PerformanceInterceptor"></plugin>
      <!-- 分页插件配置 -->
      <plugin interceptor="com.baomidou.mybatisplus.plugins.PaginationInterceptor"></plugin>
    </plugins>
  • 分页测试类(性能分析,配置后可以输出sql及取数时间):
代码语言:javascript复制
@RunWith(SpringRunner.class)
//SpringBootTest 是springboot 用于测试的注解,可指定启动类或者测试环境等,这里直接默认。
@SpringBootTest 
@Slf4j
public 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("分页结束");
    }

}
  • 性能插件体现,控制台输出:
代码语言:javascript复制
 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 注意可以在代码生成器里面配置规则的,可自动配置
代码语言:javascript复制
    /**
     * 创建时间
     */
    @TableField(fill=FieldFill.INSERT)
    private Date gmtCreate;
    /**
     * 修改时间
     */
    @TableField(fill=FieldFill.INSERT_UPDATE)
    private Date gmtModified;
  • 定义处理类:
  • MybatisObjectHandler
代码语言:javascript复制
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);
    }
}
  • 修改springb-mybatis.xml文件,加入此配置
代码语言:javascript复制
    <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"/>

再新增或者修改时,对应时间就会进行更新:

代码语言:javascript复制
 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

  • Druid是JDBC组件,包括三个部分:
    • DruidDriver: 代理Driver,能够提供基于Filter-Chain模式的插件体系
    • DruidDataSource: 高效可管理的数据库连接池
    • SQL Parser: Druid内置使用SQL Parser来实现防御SQL注入(WallFilter),合并统计没有参数化的SQL(StatFilter的mergeSql),SQL格式化,分库分表
  • Druid的作用:
    • 监控数据库访问性能: Druid内置提供了一个功能强大的StatFilter插件,能够详细统计SQL的执行性能,提升线上分析数据库访问性能
    • 替换DBCP和C3P0: Druid提供了一个高效,功能强大,可扩展性好的数据库连接池
    • 数据库密码加密: 直接把数据库密码写在配置文件容易导致安全问题,DruidDruiver和DruidDataSource都支持PasswordCallback
    • 监控SQL执行日志: Druid提供了不同的LogFilter,能够支持Common-Logging,Log4j和JdkLog,可以按需要选择相应的LogFilter,监控数据库访问情况
    • 扩展JDBC: 通过Druid提供的Filter-Chain机制,编写JDBC层的扩展
  • 配置参数: Druid的DataSource:com.alibaba.druid.pool.DruidDataSource

配置参数

缺省值

说明

name

如果存在多个数据源,监控时可以通过name属性进行区分,如果没有配置,将会生成一个名字:"DataSource-" System.identityHashCode(this)

jdbcUrl

连接数据库的url,不同的数据库url表示方式不同:mysql:jdbc:mysql://192.16.32.128:3306/druid2oracle : 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实时消费模型
代码语言:javascript复制
通过kafka消费,就是kafkaFireHose.
同时,实时节点的另外一个模块Plumer,用于Segment的生成,并且按照指定的周期,
将本周期内生成的所有数据块合并成一个
  • Segment文件从制造到传播过程:
代码语言:javascript复制
1.实时节点生产出Segment文件,并且存到文件系统中
2.Segment文件的<MetaStore>存放到Mysql等其他外部数据库中
3.Master通过Mysql中的MetaStore,通过一定的规则,将Segment分配给属于它的节点
4.历史节点得到Master发送的指令后会从文件系统中拉取属于自己的Segment文件,并且通过zookeeper,告知集群,自己提供了此块Segment的查询服务
5.实时节点丢弃Segment文件,并且声明不在提供此块文件的查询服务
历史节点
  • 历史节点再启动的时候:
    • 优先检查自己的本地缓存中是否已经有了缓存的Segment文件
    • 然后从文件系统中下载属于自己,但还不存在的Segment文件
    • 无论是何种查询,历史节点首先将相关的Segment从磁盘加载到内存.然后再提供服务
  • 历史节点的查询效率受内存空间富余程度的影响很大:
    • 内存空间富余,查询时需要从磁盘加载数据的次数减少,查询速度就快
    • 内存空间不足,查询时需要从磁盘加载数据的次数就多,查询速度就相对较慢
    • 原则上历史节点的查询速度与其内存大小和所负责的Segment数据文件大小成正比关系
查询节点
  • 查询节点便是整个集群的查询中枢:
    • 在常规情况下,Druid集群直接对外提供查询的节点只有查询节点, 而查询节点会将从实时节点与历史节点查询到的数据合并后返回给客户端
  • Druid使用了Cache机制来提高自己的查询效率.
  • Druid提供两类介质作为Cache:
    • 外部cache:Memcached
    • 内部Cache: 查询节点或历史节点的内存, 如果用查询节点的内存作为Cache,查询的时候会首先访问其Cache,只有当不命中的时候才会去访问历史节点和实时节点查询数据
协调节点
  • 对于整个Druid集群来说,其实并没有真正意义上的Master节点.
  • 实时节点与查询节点能自行管理并不听命于任何其他节点,
  • 对于历史节点来说,协调节点便是他们的Master,因为协调节点将会给历史节点分配数据,完成数据分布在历史节点之间的负载均衡.
  • 历史节点之间是相互不进行通讯的,全部通过协调节点进行通讯
  • 利用规则管理数据的生命周期:
    • Druid利用针对每个DataSoure设置的规则来加载或者丢弃具体的文件数据,来管理数据的生命周期
    • 可以对一个DataSource按顺序添加多条规则,对于一个Segment文件来说,协调节点会逐条检查规则
    • 当碰到当前Segment文件负责某条规则的情况下,协调节点会立即命令历史节点对该文件执行此规则,加载或者丢弃,并停止余下的规则,否则继续检查
索引服务

除了通过实时节点生产Segment文件之外,druid还提供了一组索引服务来摄入数据

  • 索引服务的优点:
    • 有不同的获取数据的方式,支持pull和push
    • 可以通过API编程的方式来配置任务
    • 可以更加灵活地使用资源
    • 灵活地操作Segment文件
  • 索引服务的主从架构: 索引服务包含一组组件,并以主从结构作为架构方式,统治节点 Overload node为主节点,中间管理者Middle Manager为从节点
    • Overload node: 索引服务的主节点.对外负责接收任务请求,对内负责将任务分解并下发到从节点即Middle Manager.有两种运行模式:
      • 本地模式(默认): 此模式主节点不仅需要负责集群的调度,协调分配工作,还需要负责启动Peon(苦工)来完成一部分具体的任务
      • 远程模式: 主从节点分别运行在不同的节点上,主节点只负责协调分配工作.不负责完成任务,并且提供rest服务,因此客户端可以通过HTTP POST来提交任务
代码语言:javascript复制
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工作原理

过滤器机制

  • Zuul提供了一个框架,可以对过滤器进行动态的加载,编译,运行
代码语言:javascript复制
1.Zuul的过滤器之间没有直接的相互通信,他们之间通过一个RequestContext的静态类来进行数据传递的。RequestContext类中有ThreadLocal变量来记录每个Request所需要传递的数据
2.Zuul的过滤器是由Groovy写成,这些过滤器文件被放在Zuul Server上的特定目录下面,Zuul会定期轮询这些目录,修改过的过滤器会动态的加载到Zuul Server中以便过滤请求使用
  • 标准过滤器类型: Zuul大部分功能都是通过过滤器来实现的。Zuul中定义了四种标准过滤器类型,这些过滤器类型对应于请求的典型生命周期
    • PRE: 在请求被路由之前调用,利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等
    • ROUTING: 请求路由到微服务,用于构建发送给微服务的请求,使用Apache HttpClient或Netfilx Ribbon请求微服务
    • POST: 在路由到微服务以后执行,用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等
    • ERROR: 在其他阶段发生错误时执行该过滤器
  • 内置的特殊过滤器:
    • StaticResponseFilter: StaticResponseFilter允许从Zuul本身生成响应,而不是将请求转发到源
    • SurgicalDebugFilter: SurgicalDebugFilter允许将特定请求路由到分隔的调试集群或主机
  • 自定义的过滤器: 除了默认的过滤器类型,Zuul还允许我们创建自定义的过滤器类型。如STATIC类型的过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务

过滤器的生命周期 Zuul请求的生命周期详细描述了各种类型的过滤器的执行顺序

过滤器调度过程

动态加载过滤器

Zuul的作用

Zuul可以通过加载动态过滤机制实现Zuul的功能:

  • 验证与安全保障: 识别面向各类资源的验证要求并拒绝那些与要求不符的请求
  • 审查与监控: 在边缘位置追踪有意义数据及统计结果,得到准确的生产状态结论
  • 动态路由: 以动态方式根据需要将请求路由至不同后端集群处
  • 压力测试: 逐渐增加指向集群的负载流量,从而计算性能水平
  • 负载分配: 为每一种负载类型分配对应容量,并弃用超出限定值的请求
  • 静态响应处理: 在边缘位置直接建立部分响应,从而避免其流入内部集群
  • 多区域弹性: 跨越AWS区域进行请求路由,旨在实现ELB使用多样化并保证边缘位置与使用者尽可能接近
Zuul与应用的集成方式

ZuulServlet - 处理请求(调度不同阶段的filters,处理异常等)

所有的Request都要经过ZuulServlet的处理,

Zuul对request处理逻辑的三个核心的方法:

ZuulServletZuulServlet交给ZuulRunner去执行。由于ZuulServlet是单例,因此ZuulRunner也仅有一个实例。ZuulRunner直接将执行逻辑交由FilterProcessor处理,FilterProcessor也是单例,其功能就是依据filterType执行filter的处理逻辑

FilterProcessor对filter的处理逻辑:

ContextLifeCycleFilter - RequestContext 的生命周期管理:

  • ContextLifecycleFilter的核心功能是为了清除RequestContext;请求上下文RequestContext通过ThreadLocal存储,需要在请求完成后删除该对象RequestContext提供了执行filter Pipeline所需要的Context,因为Servlet是单例多线程,这就要求RequestContext即要线程安全又要Request安全。context使用ThreadLocal保存,这样每个worker线程都有一个与其绑定的RequestContext,因为worker仅能同时处理一个Request,这就保证了Request Context 即是线程安全的由是Request安全的。

GuiceFilter - GOOLE-IOC(Guice是Google开发的一个轻量级,基于Java5(主要运用泛型与注释特性)的依赖注入框架(IOC).Guice非常小而且快.)

StartServer - 初始化 zuul 各个组件(ioc,插件,filters,数据库等)

FilterScriptManagerServlet - uploading/downloading/managing scripts, 实现热部署 Filter源码文件放在zuul 服务特定的目录, zuul server会定期扫描目录下的文件的变化,动态的读取编译运行这些filter,如果有Filter文件更新,源文件会被动态的读取,编译加载进入服务,接下来的Request处理就由这些新加入的filter处理

缓存-Redis

  • Redis: Redis是一个开源的内存中的数据结构存储系统,可以用作数据库,缓存消息中间件
  • 操作工具:Redis Desktop Manager

整合Redis缓存

  • 在pom.xml中引入redis依赖
代码语言:javascript复制
<dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 配置redis,在application.properties中配置redis
代码语言:javascript复制
spring.redis.host=192.168.32.242
  • RedisTemplate:(操作k-v都是对象)
代码语言:javascript复制
@Bean
    @ConditionalOnMissingBean(
        name = {"redisTemplate"}
    )
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
  • 保存对象时,使用JDK的序列化机制,将序列化后的数据保存到redis
  • 为了增强Redis数据库中的数据可读性:
    • 将对象数据以json方式保存:
      • 将对象转化为json
      • 配置redisTemplate的json序列化规则
代码语言:javascript复制
   @Configuration
public 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;
 }
}
代码语言:javascript复制
Redis常见的数据类型:
	String-字符串
	List-列表
	Set-集合
	Hash-散列
	ZSet-有序集合

redisTemplate.opsForValue()--String(字符串)
redisTemplate.opsForList()--List(列表)
redisTemplate.opsForSet()--Set(集合)
redisTemplate.opsForHash()--Hash(散列)
redisTemplate.opsForZSet()--ZSet(有序集合)
  • StringRedisTemplate(操作k-v都是字符串) 在RedisAutoConfiguration中:
代码语言:javascript复制
@Bean
    @ConditionalOnMissingBean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

在StringRedisTemplate中:

代码语言:javascript复制
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);
    }
}
代码语言:javascript复制
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以后,Zookeeper就进入状态同步过程:
    • leader等待server连接
    • Follower连接leader,将最大的zxid发送给leader
    • Leader根据follower的zxid确定同步点
    • 完成同步后通知follower已经成为uptodate状态
    • Follower收到uptodate消息后,又可以重新接受client的请求进行服务
工作流程
  • Leader工作流程: Leader主要有三个功能:
    • 恢复数据
    • 维持与Learner的心跳,接收Learner请求并判断Learner的请求消息类型
    • Learner的消息类型主要有PING消息,REQUEST消息,ACK消息,REVALIDATE消息,根据不同的消息类型,进行不同的处理
      • PING消息: Learner的心跳信息
      • REQUEST消息: Follower发送的提议信息,包括写请求及同步请求
      • ACK消息: Follower的对提议的回复.超过半数的Follower通过,则commit该提议
      • REVALIDATE消息: 用来延长SESSION有效时间
  • 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:
    • One-time trigger(一次性触发)
      • 当设置监视的数据发生改变时,该监视事件会被发送到客户端
      • 例如:如果客户端调用了getData("/znode1", true)并且稍后/znode1节点上的数据发生了改变或者被删除了,客户端将会获取到/znode1发生变化的监视事件,而如果/znode1再一次发生了变化,除非客户端再次对/znode1设置监视,否则客户端不会收到事件通知
    • Sent to the client(发送至客户端)
      • 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).网络延迟或者其他因素可能导致不同的客户端在不同的时刻感知某一监视事件,但是不同的客户端所看到的一切具有一致的顺序
    • The data for which the watch was set(被设置watch的数据)
      • znode 节点本身具有不同的改变方式
      • 例如:Zookeeper 维护了两条监视链表:数据监视和子节点监视(data watches and child watches) getData() and exists()设置数据监视,getChildren()设置子节点监视
      • 又例如:Zookeeper设置的不同监视返回不同的数据,getData()和exists()返回znode节点的相关信息,而getChildren()返回子节点列表.因此,setData()会触发设置在某一节点上所设置的数据监视(假定数据设置成功),而一次成功的create()操作则会出发当前节点上所设置的数据监视以及父节点的子节点监视.一次成功的delete()操作将会触发当前节点的数据监视和子节点监视事件,同时也会触发该节点父节点的child 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采用了基于多线程的架构.启动时,框架初始化一套worker线程,这套线程被调度器用来执行预定的作业.
      • Quartz依赖一套松耦合的线程池管理部件来管理线程环境
    • 任务:
      • 自己编写的业务逻辑,交给quartz执行
    • 触发器:
      • 调度作业,什么时候开始执行,什么时候结束执行

Quartz设计模式

  • Builer模式
  • Factory模式
  • 组件模式
  • 链式写法

Quartz体系结构

Quartz框架中的核心类:

  • JobDetail:
    • Quartz每次运行都会直接创建一个JobDetail,同时创建一个Job实例.
    • 不直接接受一个Job的实例,接受一个Job的实现类
    • 通过new instance()的反射方式来实例一个Job,在这里Job是一个接口,需要编写类去实现这个接口
  • Trigger:
    • 它由SimpleTrigger和CronTrigger组成
    • SimpleTrigger实现类似Timer的定时调度任务,CronTrigger可以通过cron表达式实现更复杂的调度逻辑
  • Scheduler:
    • 调度器
    • JobDetail和Trigger可以通过Scheduler绑定到一起

Quartz重要组件

Job接口
  • 可以通过实现该接口来实现我们自己的业务逻辑,该接口只有execute()一个方法,我们可以通过下面的方式来实现Job接口来实现我们自己的业务逻辑
代码语言:javascript复制
public class HelloJob implements Job{

    public void execute(JobExecutionContext context) throws JobExecutionException {
    //编写我们自己的业务逻辑
    }
JobDetail
  • 每次都会直接创建一个JobDetail,同时创建一个Job实例,它不直接接受一个Job的实例,但是它接受一个Job的实现类,通过new instance()的反射方式来实例一个Job.可以通过下面的方式将一个Job实现类绑定到JobDetail中
代码语言:javascript复制
		JobDetail jobDetail=JobBuilder.newJob(HelloJob.class).
                withIdentity("myJob", "group1")
                .build();
JobBuiler
  • 主要是用来创建JobDeatil实例
JobStore
  • 绑定了Job的各种数据
Trigger
  • 主要用来执行Job实现类的业务逻辑的,我们可以通过下面的代码来创建一个Trigger实例
代码语言:javascript复制
	CronTrigger trigger = (CronTrigger) TriggerBuilder
                .newTrigger()
                .withIdentity("myTrigger", "group1")    //创建一个标识符
                .startAt(date)//什么时候开始触发
                //每秒钟触发一次任务
                .withSchedule(CronScheduleBuilder.cronSchedule("* * * * * ? *"))

                .build();
Scheduler

创建Scheduler有两种方式

  • 通过StdSchedulerFactory来创建
代码语言:javascript复制
SchedulerFactory sfact=new StdSchedulerFactory();
Scheduler scheduler=sfact.getScheduler();
  • 通过DirectSchedulerFactory来创建
代码语言:javascript复制
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这个包下,当我们程序启动的时候,它首先会到我们的根目录下查看是否配置了该资源文件,如果没有就会到该包下读取相应信息,当我们咋实现更复杂的逻辑时,需要自己指定参数的时候,可以自己配置参数来实现
代码语言:javascript复制
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
  • quartz.properties资源文件主要组成部分:
    • 调度器属性
    • 线程池属性
    • 作业存储设置
    • 插件设置
  • 调度器属性:
    • org.quartz.scheduler.instanceName属性用来区分特定的调度器实例,可以按照功能用途来给调度器起名
    • org.quartz.scheduler.instanceId属性和前者一样,也允许任何字符串,但这个值必须是在所有调度器实例中是唯一的,尤其是在一个集群当中,作为集群的唯一key.假如想quartz生成这个值的话,可以设置为Auto
  • 线程池属性:
    • threadCount: 设置线程的数量
    • threadPriority: 设置线程的优先级
    • org.quartz.threadPool.class: 线程池的实现
  • 作业存储设置:
    • 描述了在调度器实例的声明周期中,job和trigger信息是怎么样存储的
  • 插件配置:
    • 满足特定需求用到的quartz插件的配置

监听器

对事件进行监听并且加入自己相应的业务逻辑,主要有以下三个监听器分别对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个星期三

代码语言:javascript复制
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:
    • 提供一个可视化的UI页面展示描述文件
    • 接口的调用方,测试,项目经理等都可以在该页面中对相关接口进行查阅和做一些简单的接口请求
    • 该项目支持在线导入描述文件和本地部署UI项目
  • Swagger Editor:
    • 类似于markdown编辑器用来编辑Swagger描述文件的编辑器
    • 该编辑器支持实时预览描述文件的更新效果
    • 提供了在线编辑器和本地部署编辑器两种方式
  • Swagger Inspector:
    • 在线对接口进行测试
    • 会比Swagger里面做接口请求会返回更多的信息,也会保存请求的实际请求参数等数据
  • Swagger Hub:
    • 集成上面的所有工具的各个功能
    • 可以以项目和版本为单位,将描述文件上传到Swagger Hub中,在Swagger Hub中可以完成上面项目的所有工作

Swagger注解

@Api

该注解将一个controller类标注为一个Swagger API. 在默认情况下 ,Swagger core只会扫描解析具有 @Api注解的类,而忽略其它类别的资源,比如JAX-RS endpoints, Servlets等注解. 该注解的属性有:

  • tags: API分组标签,具有相同标签的API将会被归并在一组内显示
  • value: 如果tags没有定义 ,value将作为Apitags使用
@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存储数据库变化,以authorid唯一标识一个changeSet, 支持数据库变化的合并
    • 日志文件支持多种格式. 比如XML, YAML, JSON, SQL
    • 支持多种运行方式. 比如命令行, Spring集成, Maven插件, Gradle插件
    • 在数据库中保存数据库修改历史DatabaseChangeHistory, 在数据库升级时自动跳过已应用的变化
    • 提供变化应用的回滚功能,可按时间,数量或标签tag回滚已经应用的变化
    • 可生成html格式的数据库修改文档

日志文件changeLog

  • changeLogLiquibase用来记录数据库变更的日志文件,一般放在classpath下,然后配置到执行路径中
  • changeLog支持多种格式, 主要有XML, JSON, YAML, SQL, 推荐使用XML格式
  • 一个 < changeSet > 标签对应一个变更集, 由属性id, name, changelog的文件路径唯一标识组合而成
  • changelog在执行时不是按照id的顺序,而是按照changSetchanglog中出现的顺序
  • 在执行changelog,Liquibase会在数据库中新建2张表,写执行记录:databasechangelog - changelog的执行日志databasechangeloglock - changelog锁日志
  • 在执行changelog中的changeSet时,会首先查看databasechangelog表,如果已经执行过,则会跳过,除非changeSetrunAlways属性为true, 如果没有执行过,则执行并记录changelog日志
  • changelog中的一个changeSet对应一个事务,在changeSet执行完后commit, 如果出现错误就会rollback

常用标签及命令

changeSet标签

< changeSet > 标签的主要属性有:

  • runAlways: 即使执行过,仍然每次都要执行
    • 由于databasechangelog中还记录了changeSetMD5校验值MD5SUM, 如果changeSetidname没变,而内容变化.则MD5值变化,这样即使runAlways的值为true, 也会导致执行失败报错.
    • 这时应该使用runOnChange属性
  • runOnChange: 第一次的时候以及当changeSet发生变化的时候执行,不受MD5校验值的约束
  • runInTransaction: 是否作为一个事务执行,默认为true.
    • 如果设置为false, 需要注意: 如果执行过程中出错了不会rollback, 会导致数据库处于不一致的状态

< changeSet > 有一个 < rollback > 子标签,用来定义回滚语句:

  • 对于create table, rename column, add column,Liquibase会自动生成对应的rollback语句
  • 对于drop table, insert data等需要显式定义rollback语句
include标签
  • changelog文件越来越多时,需要使用 < include > 标签将文件管理起来:
    • file: 包含的changelog文件的路径,这个文件可以是Liquibase支持的任意格式
    • relativeToChangelogFile: 相对于changelogFile的路径,表示file属性的文件路径是相对于changelogFile的而不是classpath的,默认为false
代码语言:javascript复制
<?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>
  • < include >标签存在循环引用和重复引用的问题,循环引用会导致无限循环,需要注意
includeAll标签
  • < includeAll > 标签指定的是changelog的目录,而不是文件
代码语言:javascript复制
<includeAll path="com/example/changelogs/"/>
diff命令
  • diff命令用于比较数据库之间的异同
代码语言:javascript复制
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, 需要生成当前数据的changeSet, 可以使用两种方式:
    • 使用数据库工具导出SQL数据,然后在changLog文件中以SQL格式记录
    • 使用generateChangeLog命令生成changeLog文件
代码语言:javascript复制
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使用示例

  • application.properties中配置changeLog路径:
代码语言:javascript复制
# Liquibase配置
liquibase=true
# changelog默认路径
liquibase.change-log=classpath:/db/changelog/sqlData.xml
  • xml配置sample:
代码语言:javascript复制
<?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>
  • 待执行的SQL语句 - init.sql:
代码语言:javascript复制
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'
  • 启动项目.
  • 在maven配置插件生成已有数据库的changelog文件: 需要在pom.xml中增加配置,然后配置liquibase.properties
代码语言:javascript复制
<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>
代码语言:javascript复制
changeLogFile=src/main/resources/db/changelog/sqlData.xml
driver=oracle.jdbc.driver.OracleDriver
url=jdbc:oracle:thin:@chova
username=chova
password=123456
verbose=true
# 生成文件的路径
outputChangeLogFile=src/main/resources/changelog.xml

然后执行 [ mvn liquibase:generateChangeLog ] 命令,就是生成changelog.xml文件 liquibase:update

  • 执行changeLog中的变更
代码语言:javascript复制
mnv liquibase:update

liquibase:rollback

  • rollbackCount: 表示rollbackchangeSet的个数
  • rollbackDate: 表示rollback到指定日期
  • rollbackTag: 表示rollback到指定的tag, 需要使用liquibase在具体的时间点上打上tag
    • rollbackCount示例:
代码语言:javascript复制
mvn liquibase:rollback -Dliquibase.rollbackCount=3
  • rollbackDate示例: 需要注意日期格式,必须匹配当前平台执行DateFormat.getDateInstance() 得到的格式,比如 MMM d, yyyy
代码语言:javascript复制
mvn liquibase:rollback -Dliquibase.rollbackDate="Apr 10, 2020"
  • rollbackTag示例: 使用tag标识,需要先打tag, 然后rollback到tag
代码语言:javascript复制
mvn liquibase:tag -Dliquibase.tag=tag20200410

mvn liquibase:rollback -Dliquibase.rollbackTag=tag20200410

flyway

flyway基本概念

  • flyway是一款数据库版本控制管理工具,支持数据库版本自动升级,不仅支持Command LineJava API, 同时也支持Build构建工具和SpringBoot, 也可以在分布式环境下安全可靠地升级数据库,同时也支持失败恢复
  • flyway是一款数据库迁移 (migration) 工具,也就是在部署应用的时候,执行数据库脚本的应用,支持SQLJava两种类型的脚本,可以将这些脚本打包到应用程序中,在应用程序启动时,由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 PostgresAmazon RDS
  • 自动迁移: 使用flyway提供的API, 可以让应用启动和数据库迁移同时工作
  • 快速失败: 损坏的数据库或失败的迁移可以防止应用程序启动
  • 数据库清理: 在一个数据库中删除所有的表,视图,触发器. 而不是删除数据库本身
SQL脚本
  • 格式 : V 版本号 双下划线 描述 结束符
代码语言:javascript复制
V1_INIT_DATABASE.sql
  • V是默认值,可以进行自定义配置:
代码语言:javascript复制
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
  • flyway中最核心的就是用于记录所有版本演化和状态的Metadata表
  • flyway首次启动时会创建默认表名为SCHEMA_VERSION的元数据表,表结构如下:

列名

类型

是否为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
  • flyway将每一个数据库脚本称之为migration,flyway主要支持两种类型的migrations:
    • Versioned migrations:
      • 最常用的migration,用于版本升级
      • 每一个版本都有一个唯一的标识并且只能被应用一次,并且不能再修改已经加载过的Migrations,因为Metadata表会记录Checksum值
      • version标识版本号由一个或多个数字构成,数字之间的分隔符可以采用点或下划线,在运行时下划线其实也是被替换成点了,每一部分的前导数字0都会被自动忽略
    • Repeatable migrations:
      • 指的是可重复加载的Migrations,每一次的更新会影响Checksum值,然后都会被重新加载,并不用于版本升级.对于管理不稳定的数据库对象更新时非常有用
      • Repeatable的Migrations总是在Versioned的Migrations之后按顺序执行,开发者需要维护脚本并且确保可以重复执行.通常会在sql语句中使用CREATE OR REPLACE来确保可重复执行
  • Migration命名规范:
  1. prefix: 前缀标识.可以配置,默认情况下: V - Versioned, R - Repeatable
  2. version: 标识版本号. 由一个或多个数字构成,数字之间的分隔符可以使用点或者下划线
  3. separator: 用于分割标识版本号和描述信息. 可配置,默认情况下是两个下划线 _ _
  4. description: 描述信息. 文字之间可以用下划线或空格分割
  5. suffix: 后续标识. 可配置,默认为 .sql
  • 确保版本号唯一 ,flyway按照版本号顺序执行 . repeatable没有版本号,因为repeatable migration会在内容改变时重复执行
  • 默认情况下 ,flyway会将单个migration放在一个事务里执行,也可以通过配置将所有migration放在同一个事务里执行
  • 每个Migration支持两种编写方式:
    • Java API
    • SQL脚本
  • Java API: 通过实现org.flywaydb.core.api.migration.jdbc.JdbcMigration接口来创建一个Migration, 也就是通过JDBC来执行SQL, 对于类是CLOB或者BLOB这种不适合在SQL中实现的脚本比较方便
代码语言:javascript复制
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();
		}
	}
}
  • SQL脚本: 简单的SQL脚本文件
代码语言:javascript复制
// 单行命令
CREATE TABLE user (name VARCHAR(25) NOT NULL, PRIMARY KEY(name));
 
 // 多行命令
 -- Placeholder
 INSERT INTO ${tableName} (name) VALUES ("oxford");
Callbacks
  • flyway在执行migration时提供了一系列的hook, 可以在执行过程中进行额外的操作:

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

  • 只要将migration的名称以hook开头,这些hook就可以执行SQL和Java类型的migrations:
    • SQL类型的hook:
      • beforeMigrate.sql
      • beforeEachMigrate.sql
      • beforeRepair_vacuum.sql
    • Java类型的hook需要实现接口 : org.flyway.core.api.callback.CallBack
flyway中6种命令
  • Migrate:
    • 将数据库迁移到最新版本,是flyway工作流的核心功能.
    • flywayMigrate时会检查元数据Metadata表.如果不存在会创建Metadata表,Metadata表主要用于记录版本变更历史以及Checksum之类
    • Migrate时会扫描指定文件系统或classpath下的数据库的版本脚本Migrations, 并且会逐一比对Metadata表中已经存在的版本记录,如果未应用的Migrations,flyway会获取这些Migrations并按次序Apply到数据库中,否则不会做任何事情
    • 通常会在应用程序启动时默认执行Migrate操作,从而避免程序和数据库的不一致
  • Clean:
    • 来清除掉对应数据库的Schema的所有对象 .flyway不是删除整个数据库,而是清除所有表结构,视图,存储过程,函数以及所有相关的数据
    • 通常在开发和测试阶段使用,能够快速有效地更新和重新生成数据库表结构.但是不应该在production的数据库使用
  • Info:
    • 打印所有Migrations的详细和状态信息,是通过Metadata表和Migrations完成的
    • 能够快速定位当前数据库版本,以及查看执行成功和失败的Migrations
  • Validate:
    • 验证已经ApplyMigrations是否有变更 ,flyway是默认开启验证的
    • 操作原理是对比Metadata表与本地MigrationChecksum值,如果相同则验证通过,否则验证失败,从而可以防止对已经Apply到数据库的本地Migrations的无意修改
  • Baseline:
    • 针对已经存在Schema结构的数据库的一种解决方案
    • 实现在非空数据库中新建Metadata表,并将Migrations应用到该数据库
    • 可以应用到特定的版本,这样在已有表结构的数据库中也可以实现添加Metadata表,从而利用flyway进行新的Migrations的管理
  • Repair:
    • 修复Metadata表,这个操作在Metadata表表现错误时很有用
    • 通常有两种用途:
      • 移除失败的Migration记录,这个问题针对不支持DDL事务的数据库
      • 重新调整已经应用的MigrationsChecksums的值. 比如,某个Migration已经被应用,但本地进行了修改,又期望重新应用并调整Checksum值. 不建议对数据库进行本地修改

flyway的使用

正确创建Migrations
  • Migrations: flyway在更新数据库时使用的版本脚本
    • 一个基于sqlMigration命名为V1_ _init_tables.sql, 内容即为创建所有表的sql语句
    • flyway也支持基于JavaMigration
    • flyway加载Migrations的默认Locationsclasspath:db/migration, 也可以指定filesystem:/project/folder. Migrations的加载是在运行时自动递归执行的
  • 除了指定的Locations外,flyway需要遵从命名格式对Migrations进行扫描,主要分为两类:
    • Versioned migrations:
      • Versioned类型是常用的Migration类型
      • 用于版本升级,每一个版本都有一个唯一的标识并且只能被应用一次. 并且不能再修改已经加载过的Migrations, 因为Metadata表会记录Checksum
      • 其中的version标识版本号,由一个或者多个数字构成,数字之间的分隔符可以采用点或者下划线,在运行时下划线也是被替换成点了. 每一部分的前导零都会被省略
    • Repeatable migrations:
      • Repeatable是指可重复加载的Migrations, 其中每一次更新都会更新Checksum值,然后都会被重新加载,并不用于版本升级. 对于管理不稳定的数据库对象的更新时非常有用
      • RepeatableMigrations总是在Versioned之后按顺序执行,开发者需要维护脚本并确保可以重复执行,通常会在sql语句中使用CREATE OR REPLACE来保证可重复执行
flyway数据库
  • flyway支持多种数据库:
    • Oracle
    • SQL Server
    • SQL Azure
    • DB2
    • DB2 z/OS
    • MySQL
    • Amazon RDS
    • Maria DB
    • Google Cloud SQL
    • PostgreSQL
    • Heroku
    • Redshift
    • Vertica
    • H2
    • Hsql
    • Derby
    • SQLite
    • SAP HANA
    • solidDB
    • Sybase ASE and Phoenix
  • 目前主流使用的数据库有MySQL,H2,HsqlPostgreSQL. 对应的flyway.url配置如下:
代码语言:javascript复制
# MySQL
flyway.url=jdbc:mysql://localhost:3306/db?serverTimezone=UTC&useSSL=true

# H2
flyway.url=jdbc:h2:./.tmp/db

# Hsql
flyway.url=jdbc:hsqldb:hsql//localhost:1476/db

# PostgreSQL
flyway.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
  • 引入flyway依赖:
代码语言:javascript复制
<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>
  • 创建的springboot的maven项目,配置数据源信息:
代码语言:javascript复制
server.port=8080
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/db
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
  • 在classpath目录下新建/db/migration文件夹,并创建SQL脚本:
代码语言:javascript复制
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=utf8

insert 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加载了SQL脚本并执行
  • 查看数据库:
    • 默认情况下,生成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容器更具有便携性,能够高效地利用服务器
    • 容器更多的是用于表示软件的一个标准化单元,由于容器的标准化,因此可以无视基础设施的差异,部署到任何一个地方
    • Docker也为容器提供更强的业界隔离兼容
  • Docker利用Linux内核中的资源分离机制cgroups以及Linux内核的namespace来创建独立的容器containers
    • 可以在Linux实体下运作,避免引导一个虚拟机造成的额外负担
    • Linux内核对namespace的支持可以完全隔离工作环境下的应用程序,包括:
      • 线程树
      • 网络
      • 用户ID
      • 挂载文件系统
    • Linux内核的cgroups提供资源隔离,包括:
      • CPU
      • 存储器
      • block I/O
      • 网络

Docker基础架构

Docker引擎
  • Docker引擎: Docker Engine
    • 是一个服务端 - 客户端结构的应用
    • 主要组成部分:
      • Docker守护进程: Docker daemons,也叫dockerd.
        • 是一个持久化进程,用户管理容器
        • Docker守护进程会监听Docker引擎API的请求
      • Docker引擎API: Docker Engine API
        • 用于与Docker守护进程交互使用的API
        • 是一个RESTful API,不仅可以被Docker客户端调用,也可以被wget和curl等命令调用
      • Docker客户端: docker
        • 是大部分用户与Docker交互的主要方式
        • 用户通过客户端将命令发送给守护进程
        • 命令遵循Docker Engine API
Docker注册中心
  • Docker注册中心: Docker registry,用于存储Docker镜像
  • Docker Hub: Docker的公共注册中心,默认情况下,Docker在这里寻找镜像.也可以自行构建私有的注册中心
Docker对象
  • Docker对象指的是 :Images,Containers,Networks, Volumes,Plugins等等
    • 镜像: Images
      • 一个只读模板,用于指示创建容器
      • 镜像是分层构建的,定义这些层次的文件叫作Dockerfile
    • 容器: Containers
      • 镜像可运行的实例
      • 容器可以通过API或者CLI(命令行)进行操作
    • 服务: Services
      • 允许用户跨越不同的Docker守护进程的情况下增加容器
      • 并将这些容器分为管理者(managers)和工作者(workers),来为swarm共同工作

Docker扩展架构

Docker Compose
  • Docker Compose是用来定义和运行多个容器Docker应用程序的工具
  • 通过Docker Compose, 可以使用YAML文件来配置应用程序所需要的所有服务,然后通过一个命令,就可以创建并启动所有服务
  • Docker Compose对应的命令为 : docker-compose
Swarm Mode
  • Docker 1.12以后 ,swarm mode集成到Docker引擎中,可以使用Docker引擎APICLI命令直接使用
  • Swarm Mode内置 k-v 存储功能,特点如下:
    • 具有容错能力的去中心化设计
    • 内置服务发现
    • 负载均衡
    • 路由网格
    • 动态伸缩
    • 滚动更新
    • 安全传输
  • Swarm Mode的相关特性使得Docker本地的Swarm集群具备与Mesos.Kubernetes竞争的实力
  • cluster: 集群
    • Docker将集群定义为 - 一群共同作业并提供高可用性的机器
  • swarm:
    • 一个集群的Docker引擎以swarm mode形式运行
      • swarm mode是指Docker引擎内嵌的集群管理和编排功能
      • 当初始化一个cluster中的swarm或者将节点加入一个swarm,Docker引擎就会以swarm mode的形式运行
  • Swarm Mode原理:
    • swarm中的Docker机器分为两类:
      • managers: 管理者. 用于处理集群关系和委派
      • workers: 工作者. 用于执行swarm服务
        • 当创建swarm服务时,可以增加各种额外的状态: 数量,网络,端口,存储资源等等
        • Docker会去维持用户需要的状态:
          • 比如,一个工作节点宕机后,那么Docker就会把这个节点的任务委派给另外一个节点
          • 这里的任务task是指: 被swarm管理者管理的一个运行中的容器
  • swarm相对于单独容器的优点:
    • 修改swarm服务的配置后无需重启
    • Dockerswarm mode形式运行时,可以选择直接启动单独的容器
    • swarm mode下,可以通过docker stack deploy使用Compose文件部署应用栈
  • swarm服务分为两种:
    • replicated services: 可以指定节点任务的总数量
    • global services: 每个节点都会运行一个指定任务
  • swarm管理员可以使用ingress负载均衡使服务可以被外部接触
  • swarm管理员会自动地给服务分配PublishedPort, 或者手动配置.
    • 外部组件,比如云负载均衡器能通过集群中任何节点上的PublishedPort去介入服务,无论服务是否启动
  • Swarm Mode有内部DNS组件,会为每个服务分配一个DNS条目 . swarm管理员使用internal load balancing去分发请求时,就是依靠的这个DNS组件
  • Swarm Mode功能是由swarmkit提供的,实现了Docker的编排层,使得swarm可以直接被Docker使用

文件格式

  • Docker有两种文件格式:
    • Dockerfile: 定义了单个容器的内容和启动时候的行为
    • Compose文件: 定义了一个多容器应用
Dockerfile

Docker可以依照Dockerfile的内容,自动化地构建镜像

  • Dockerfile包含着用户想要如何构建镜像的所有命令的文本
代码语言:javascript复制
FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py
  • RUN:
    • RUN会在当前镜像的顶层上添加新的一层layer,并在该层上执行命令,执行结果将会被提交
    • 提交后的结果将会应用于Dockerfile的下一步
  • ENTRYPOINT:
    • 入口点
    • ENTRYPOINT允许配置容器,使之成为可执行程序. 即ENTRYPOINT允许为容器增加一个入口点
    • ENTRYPOINT与CMD类似,都是在容器启动时执行,但是ENTRYPOINT的操作稳定并且不可被覆盖
    • 通过在命令行中指定 - -entrypoint命令的方式,可以在运行时将Dockerfile文件中的ENTRYPOINT覆盖
  • CMD:
    • command的缩写
    • CMD用于为已经创建的镜像提供默认的操作
    • 如果不想使用CMD提供的默认操作,可以使用docker run IMAGE [:TAG|@DIGEST] [COMMAND] 进行替换
    • 当Dockerfile拥有入口点的情况下,CMD用于为入口点赋予参数
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变量:
代码语言:javascript复制
db:
 image: "postgres:${POSTGRES_VERSION}" 
  • Compse文件可通过自身的ARGS变量,将参数传递给Dockerfile中的ARGS指令

网络

bridge
  • Docker中的网桥使用的软件形式的网桥
  • 使用相同的网桥的容器连接进入该网络,非该网络的容器无法进入
  • Docker网桥驱动会自动地在Docker主机上安装规则,这些规则使得不同桥接网络之间不能直接通信
  • 桥接经常用于:
    • 在单独容器上运行应用时,可以通过网桥进行通信
  • 网桥网络适用于容器运行在相同的Docker守护进程的主机上
  • 不同Docker守护进程主机上的容器之间的通信需要依靠操作系统层次的路由,或者可以使用overlay网络进行代替
  • bridge: 网桥驱动
    • 是Docker默认的网络驱动,接口名为docker0
    • 当没有为容器指定一个网络时,Docker将使用这个驱动
    • 可以通过daemon.json文件修改相关配置
  • 自定义网桥可以通过 brtcl 命令进行配置
host
  • host: 主机模式
    • 用于单独容器,该网络下容器只能和Docker主机进行直接连接
    • 这种host主机模式只适用于Docker 17.06以后版本的swarm服务
  • host网络和VirtualBox的仅主机网络Host-only Networking类似
overlay
  • overlay: 覆盖模式
    • 网络驱动将会创建分布式网络,该网络可以覆盖若干个Docker守护进程主机
    • overlay是基于主机特定网络host-specific networks, 当加密功能开启时,允许swarm服务和容器进行安全通信
    • 在覆盖网络overlay下,Docker能够清晰地掌握数据包路由以及发送接收容器
  • overlay有两种网络类型网络:
    • ingress: 是可掌控swarm服务的网络流量, ingress网络是overlay的默认网络
    • docker_gwbridge: 网桥网络, docker_gwbridge网络会将单独的Docker守护进程连接至swarm里的另外一个守护进程
  • overlay网络下:
    • 单独的容器swarm服务的行为和配置概念是不一样的
  • overlay策略不需要容器具有操作系统级别的路由,因为Docker负责路由
macvlan
  • macvlan:
    • 允许赋予容器MAC地址
    • 在该网络里,容器会被认为是物理设备
none
  • 在该策略下,容器不使用任何网络
  • none常常用于连接自定义网络驱动的情况下
其它网络策略模式
  • 要想运用其它网络策略模式需要依赖其它第三方插件

数据管理

  • 在默认情况下,Docker所有文件将会存储在容器里的可写的容器层container layer:
    • 数据与容器共为一体: 随着容器的消失,数据也会消失. 很难与其它容器程序进行数据共享
    • 容器的写入层与宿主机器紧紧耦合: 很难移动数据到其它容器
    • 容器的写入层是通过存储驱动storage driver管理文件系统: 存储驱动会使用Linux内核的链合文件系统union filesystem进行挂载,相比较于直接操作宿主机器文件系统的数据卷,这个额外的抽象层会降低性能
  • 容器有两种永久化存储方式:
    • volumes:
    • bind mounts: 绑定挂载
  • Linux中可以使用tmpfs进行挂载, windows用户可以使用命名管道named pipe.
  • 在容器中,不管使用哪种永久化存储,表现形式都是一样的
  • 卷: volumes.
    • 是宿主机器文件系统的一部分
    • Docker进行管理. 在Linux中,卷存储于 /var/lib/docker/volumes/
    • Docker程序不应该去修改这些文件
  • Docker推荐使用卷进行持久化数据
  • 卷可以支持卷驱动volume drivers: 该驱动允许用户将数据存储到远程主机云服务商cloud provider或其它
  • 没有名字的卷叫作匿名卷anonymous volume. 有名字的卷叫作命名卷named volume. 匿名卷没有明确的名字,当被初始化时,会被赋予一个随机名字
绑定挂载
  • 绑定挂载: bind mounts
    • 通过将宿主机器的路径挂载到容器里的这种方式,从而实现数据持续化,因此绑定挂载可将数据存储在宿主机器的文件系统中的任何地方
    • 非Docker程序可以修改这些文件
    • 绑定挂载在Docker早起就已经存在,与卷存储相比较,绑定挂载十分简单明了
    • 在开发Docker应用时,应使用命名卷named volume代替绑定挂载,因为用户不能对绑定挂载进行Docker CLI命令操作
  • 绑定挂载的使用场景:
    • 同步配置文件
      • 将宿主机的DNS配置文件(/etc/resolv.conf)同步到容器中
    • 在开发程序过程中,将源代码或者Artifact同步至容器中. 这种用法与Vagrant类似
tmpfs挂载
  • tmpfs挂载: tmpfs mounts
    • 仅仅存储于内存中,不操作宿主机器的文件系统.即不持久化于磁盘
    • 用于存储一些非持久化状态,敏感数据
      • swarm服务通过tmpfs将secrets数据(密码,密钥,证书等)存储到swarm服务
命名管道
  • 命名管道: named pipes
    • 通过pipe挂载的形式,使Docker主机和容器之间互相通讯
      • 在容器内运行第三方工具,并使用命名管道连接到Docker Engine API
覆盖问题
  • 挂载空的卷至一个目录中,目录中你的内容会被复制于卷中,不会覆盖
  • 如果挂载非空的卷绑定挂载至一个目录中,那么该目录的内容将会被隐藏obscured,当卸载后内容将会恢复显示

日志

  • 在Linux和Unix中,常见的I/O流分为三种:
    • STDIN: 输入
    • STDOUT: 正常输出
    • STDERR: 错误输出
  • 默认配置下,Docker的日志所记载的是命令行的输出结果:
    • STDOUT : /dev/stdout
    • STDERR : /dev/stderr
  • 也可以在宿主主机上查看容器的日志,使用命令可以查看容器日志的位置:
代码语言:javascript复制
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使用配置

  • 登录jenkins,点击新建,创建一个新的构建任务:
  • 跳转到新建界面:
    • 任务名称可以自行设定,但需要全局唯一
    • 输入名称后,选择构建一个自由风格的软件项目
    • 点击下方的创建按钮
    • 这样就创建了一个构建任务,然后会跳转到该任务的配置页面
  • 在构建任务页面,可以看到几个选项:
    • General
    • 源码管理
    • 构建触发器
    • 构建环境
    • 构建
    • 构建后操作
General
  • General用于构建任务的一些基本配置: 名称,描述等
  • 项目名称: 刚才创建构建任务设置的名称,可以在这里进行修改
  • 描述: 对构建任务的描述
  • 丢弃旧的构建: 服务资源是有限的,如果保存太多的历史构建,会导致jenkins速度变慢,并且服务器硬盘资源也会被占满
    • 保持构建天数: 可以自定义,根据实际情况确定一个合理的值
    • 保持构建的最大个数: 可以自定义,根据实际情况确定一个合理的值
源码管理
  • 源码管理用于配置代码的存放位置
  • 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: 周期性地构建.每隔一段时间进行构建
    • 日程表: 类似linux cronttab书写格式. 下图表示每隔30分钟进行一次构建
    • Build when a change is pushed to Gitlab: 常用的构建触发器,当有代码push到gitlab代码仓库时就进行构建
      • webhooks: 触发构建的地址,需要将这个地址配置到gitlab中
    • Poll SCM: 这个功能需要与上面的这个功能配合使用. 当代码仓库发生变动时,jekins并不知道. 这时,需要配置这个选项,周期性地检查代码仓库是否发生变动
构建环境
  • 构建环境: 构建之前的准备工作. 比如指定构建工具,这里使用Ant
  • With Ant: 选择这个选项,并指定Ant版本和JDK版本. 需要事先在jenkins服务器上安装这两个版本的工具,并且在jenkins全局工具中配置好
构建
  • 点击下方的增加构建步骤:

这里有多种增加构建步骤的方式,在这里介绍Execute shellInvoke 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配置
  • 登录jenkins
  • 系统管理
  • 系统设置
  • SSH Servers: jenkins服务器公钥文件配置好之后新增SSH Server只需要配置这一个选项即可
    • name: 服务名称.自定义,需要全局唯一
    • HostName: 主机名. 直接使用IP地址即可
    • Username: 新增Server的用户名,这里配置的是root
    • Remote Directory: 远程目录. jenkins服务器发送文件给新增的server时默认在这个目录

Ant配置文件 - build.xml

  • Ant构建配置文件build.xml :
  • project name: 项目名称. 和jenkins所构建的项目名称对应
  • target name="build": 构建的名称. 和jekins构建步骤中的targets对应.
    • depends: 指明构建需要进行的一些操作
  • property: 用来设置变量
  • fileset: 指明一个文件夹
    • include: 指明需要包含的文件
    • exclude: 指明不需要包含的文件
    • tar: 打包这个文件夹匹配到的文件
  • target: 实际的操作步骤:
    • make_runtime: 创建一些目录
    • phpcs: 利用PHP_CodeSniffer工具对PHP代码规范与质量检查工具
  • target name="tar": 打包文件
    • 因为build中没有包含这个target.所以默认情况下,执行build是不会打包文件的
    • 所以在jenkins配置界面中Ant构建步骤中的[targets],才会有[build]和[tar]这两个targets
    • 如果build.xml中build这个target depends中已经包含tar, 就不需要在jenkins中增加tar

配置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: 灰色. 构建已禁用
构建稳定性
  • 构建稳定性用天气表示: 天气越好表示构建越稳定
    • 晴转多云
    • 多云
    • 小雨
    • 雷阵雨
构建历史界面
  • console output: 输出构建的日志信息

jenkins权限管理

  • jenkins中默认的权限管理体系不支持用户组和角色配置,因此需要安装第三方插件来支持角色的配置
  • 使用Role Strategy Plugin进行权限管理:
  • 项目视图:
  • 安装Role Strategy Plugin插件
  • 安装Role Stratey Plugin后进入系统设置页面,按照如下配置后,点击 [保存] :
  • 点击 [系统管理] -> [Manage and Assign Roles] 进入角色管理页面:
  • 选择 [Manager Roles], 按照下图配置后点击 [保存]:
  • job_read只加overallread权限
  • job_create只加jobcreate权限
  • project roles中Pattern正则表达式和脚本里的是不一样的:
    • 比如过滤TEST开头的jobs,要写成 : TEST.,而不是 TEST
  • 进入[系统设置] -> [Manage and Assign Roles] -> [Assign Roles] , 按照如下模板配置后,点击 [保存]
  • Anonymous必须变成用户,给job_create组和job_read组权限,否则将没有OverAllread权限
  • project roles: 用于对应用户不同的权限
  • 验证: 登录对应的用户权限后查看用户相关权限
  • 视图通过正则表达式过滤job: 设置正则表达式为wechat.*,表示过滤所有以wechat开头的项目
  • 设置后的效果如图:

自动化测试-TestNG

TestNG基本概念

  • TestNG是一个Java语言的开源测试框架,类似JUnit和NUnit,但是功能强大,更易于使用
  • TestNG的设计目标是为了覆盖更广泛的测试类别范围:
    • 单元测试
    • 功能测试
    • 端到端测试
    • 集成测试
  • TestNG的主要功能:
    • 支持注解
    • 支持参数化和数据驱动测试: 使用@DataProvider或者XML配置
    • 支持同一类的多个实例: @Factory
    • 灵活的执行模式:
      • TestNG的运行,既可以通过Antbuild.xml: 有或这没有一个测试套定义. 又可以通过带有可视化效果的IDE插件
      • 不需要TestSuite类,测试包,测试组以及选择运行的测试. 都通过XML文件来定义和配置
    • 并发测试:
      • 测试可以运行在任意大的线程池中,并有多种运行策略可以选择: 所有方法都有自己的线程,或者每一个测试类一个线程等等
      • 测试代码是否线程安全
    • 嵌入BeanShell可以获得更大的灵活性
    • 默认使用JDK运行和相关日志功能,不需要额外增加依赖
    • 应用服务器测试的依赖方法
    • 分布式测试: 允许在从机上进行分布式测试

TestNG环境配置

  • 配置好主机的Java环境,使用命令 java -version查看
  • 在TestNG官网,下载TestNG对应系统下的jar文件
  • 系统环境变量中添加指向jar文件的路径
  • 在IDEA中安装TestNG

TestNG的基本用法

代码语言:javascript复制
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类

代码语言:javascript复制
<?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中,同时使用parallelthread-count:
    • parallel: 指定并行测试范围tests,methods,classes
    • thread-count: 并行线程数
  • preserve-order: 当设置为true时,节点下的方法按顺序执行
  • verbose: 表示记录日志的级别,在0 - 10之间取值
  • < parameter name="userName", value="15952031403" > : 给测试代码传递键值对参数,在测试类中通过注解 @Parameter({"userName"}) 获取

参数化测试

  • 当测试逻辑一样,只是参数不一样时,可以采用数据驱动测试机制,避免重复代码
  • TestNG通过 @DataProvider实现数据驱动
  • 使用@DataProvider做数据驱动:
    • 数据源文件可以是EXCEL,XML,甚至可以是TXT文本
    • 比如读取xml文件:
      • 通过@DataProvider读取XML文件中的数据
      • 然后测试方法只要标示获取数据来源的DataProvider
      • 对应的DataProvider就会将读取的数据传递给该test方法
构建XML数据文件
代码语言:javascript复制
<?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文件
代码语言:javascript复制
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类
代码语言:javascript复制
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
代码语言:javascript复制
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"));
    }
}
  • xml中的父节点与test的方法名对应:
    • xml中同名父节点的个数就意味着该test方法会被重复执行多少次
  • 当DataProvider与test方法不在同一个类时,需要指明DataProvider类:
    • @Test(dataProvider="dataProvider", dataProviderClass= GenerateData.class)

TestNG重写监听类

  • TestNG会监听每个测试用例的运行结果.可以使用监听定制一些自定义的功能,比如自动截图,发送数据给服务器:
    • 新建一个继承自TestListenerAdapter的类
    • 重写完成后,在test方法前添加 @Listener(TestNGListener.class) 注解
代码语言:javascript复制
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);
    }
}
  • 运行测试
代码语言:javascript复制
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();
        }
    }
    }
}

0 人点赞