参数校验框架使用说明

简介

在web项目和service项目中,有许多校验参数并返回结果的代码,比如:

屏幕快照 2018-01-16 下午5.27.49

这种校验代码比较冗长,费时费力且影响代码可读性。为此,提供一套参数校验框架,通过注解进行参数校验。

目前处于内测阶段,所处项目及分支:

thor:validate分支

avatar:validate分支

配置方式

spring配置文件中配置AOP:
<aop:aspectj-autoproxy proxy-target-class="true"/>

<bean id="validateAspect" class="com.aixuexi.thor.validate.aspect.ValidationAspect"/>

<aop:config>
    <aop:aspect ref="validateAspect">
        <aop:around method="around" pointcut="execution(* com.aixuexi..*.*(..))"/>
    </aop:aspect>
</aop:config>
注意:web项目只能在springmvc的配置文件中配置。

使用方式

一、校验model域

1、在形参上加 @Validate 注解,表示该参数需要校验。groups参数表示校验组。
@RequestMapping(value = "/not_null", method = RequestMethod.GET)
@ResponseBody
public ResultData test(@Validate(groups = Group.WEB) PageParam pageParam) {
    return new ResultData("invoke success!");
}
2、在model域上添加校验注解,groups的值与@Validate注解groups的值有交集时才触发校验。
public class PageParam implements Serializable {

    @NotNull(groups = {Group.WEB}, successMessage = "ABC validate success")
    private Integer pageNum;
}

 二、校验形参

在形参上加校验注解,表示该参数需要校验。
@RequestMapping(value = "/not_null", method = RequestMethod.GET)
@ResponseBody
public ResultData test(@NotNull(errorMessage = "str1不能为null!") String str) {
    return new ResultData("invoke success!");
}

注解说明

注解
支持类型
说明
@Validate
Object
表示方法形参需要校验;
表示POJO对象需要校验;
表示Collection中的对象需要校验;
@Null
Object
必须为null
@NotNull
Object
不能为null
@Assert
Boolean
是否为true或false
@Range
Number
数字是否在区间内
@NotBlank
String
字符串不为空串
@Pattern
String
字符串是否匹配正则表达式
@ValidStr
String
字符串是否合法(仅含数字、英文、汉字、下划线)
@Length
String
字符串长度是否在区间内
@Telephone
String
字符串是否为手机号格式
@Email
String
字符串是否为email格式
@NotEmpty
Collection
集合是否为空
@Furture
Date
日期是否为未来时间
@Past
Date
日期是否为过去时间

目录机构

 参数校验框架目录结构

 

关于项目中.gitignore文件中*.properties一行的思考

在做项目重构过程中发现许多项目为了把dev下的配置文件忽略(因为本地配置经常改动),在.gitignore文件中加入“*.properties”来解决。

这样做固然固然很简单方便,但在思考一下,把所有的properties文件一棒子打死了(尤其是我们的gerrit还不能在线预览文件)。其实有这样一种场景,当其它环境的properties文件中有更新,你(也可能是你的队友)写完后本地测试没问题,然后commit 上去,发现非本地环境就是没有跑通,也许你会很快意识到.gitignore文件的问题,但当你本次提交有很多改动混合在一起的时候,你会首先怀疑自己的代码有问题,然后排查了半天,还是不知所以然。。。然后半天过去了。。。发现自己被很弱智的问题坑了。。。是的这是个很忧伤的故事,蛋蛋的忧伤

所以这个.gitignore文件加东西要慎重,太随意的话,在你忘记了你忽略了什么的时候,它就拐回来坑你,还有你的队友

回到我们的问题场景,我们只是为了忽略dev/下的配置文件,打死的范围不要多也不要少,来吧1,2,3走起
1.在”.gitignore”文件中加入”src/**/dev/”
2.清除git版本对要忽略文件的已有追踪
“`
git rm -r –cached src/**/dev/
“`

这时候执行git status查看发现,dev/下的文件被删除了,像下边这样

图片1

看到这个不必困扰,其实这里只是删除了git版本库中对这些文件的记录(看一下dev目录的这些文件,他们还在)。
3.别忘了最后一步add and commit
“`
git add .
git commit -m ‘fix ignore that only ignore dev/*’
git push origin head:refs/for/master
“`

在gerrit中+2并入分支

 

好了,解决,其它忽略类似

airflow源码分析之BashOperator

BashOperator主要的功能是执行shell命令或者shell脚本。负责具体的执行过程的是BashOperator.execute()函数。
airflow的bash_operator.py文件:

from builtins import bytes
import os
import signal
from subprocess import Popen, STDOUT, PIPE
from tempfile import gettempdir, NamedTemporaryFile

from airflow.exceptions import AirflowException
from airflow.models import BaseOperator
from airflow.utils.decorators import apply_defaults
from airflow.utils.file import TemporaryDirectory


class BashOperator(BaseOperator):
    """
    :param xcom_push: If xcom_push is True, the last line written to stdout
        will also be pushed to an XCom when the bash command completes.
    :type xcom_push: bool
    :param env: If env is not None, it must be a mapping that defines the
        environment variables for the new process; these are used instead
        of inheriting the current process environment, which is the default
        behavior. (templated)
    :type env: dict
    :type output_encoding: output encoding of bash command
    """
    template_fields = ('bash_command', 'env')
    template_ext = ('.sh', '.bash',)
    ui_color = '#f0ede4'

    @apply_defaults    # 处理默认的参数
    def __init__(
            self,
            bash_command, # string 可以是单独的命令,或者是命令集,或者是.sh文件
            xcom_push=False,  # 如果两个operator有依赖关系时,值为True
            env=None,
            output_encoding='utf-8',   
            *args, **kwargs):

        super(BashOperator, self).__init__(*args, **kwargs)
        self.bash_command = bash_command
        self.env = env
        self.xcom_push_flag = xcom_push
        self.output_encoding = output_encoding

    def execute(self, context):
        """
        Execute the bash command in a temporary directory
        which will be cleaned afterwards
        """
        bash_command = self.bash_command
        self.log.info("Tmp dir root location: \n %s", gettempdir()) # 基类继承了处理log的mixin类
        with TemporaryDirectory(prefix='airflowtmp') as tmp_dir:
            with NamedTemporaryFile(dir=tmp_dir, prefix=self.task_id) as f:

                f.write(bytes(bash_command, 'utf_8'))
                f.flush()   # 将缓冲区的数据写入到磁盘中
                fname = f.name
                script_location = tmp_dir + "/" + fname
                self.log.info(
                    "Temporary script location: %s",
                    script_location
                )
                def pre_exec():
                    # Restore default signal disposition and invoke setsid
                    for sig in ('SIGPIPE', 'SIGXFZ', 'SIGXFSZ'):
                        if hasattr(signal, sig):
                            signal.signal(getattr(signal, sig), signal.SIG_DFL)
                    os.setsid()
                self.log.info("Running command: %s", bash_command)
                sp = Popen(
                    ['bash', fname],
                    stdout=PIPE, stderr=STDOUT,
                    cwd=tmp_dir, env=self.env,
                    preexec_fn=pre_exec)

                self.sp = sp

                self.log.info("Output:")
                line = ''
                for line in iter(sp.stdout.readline, b''):
                    line = line.decode(self.output_encoding).strip()
                    self.log.info(line)
                sp.wait()
                self.log.info(
                    "Command exited with return code %s",
                    sp.returncode
                )

                if sp.returncode:
                    raise AirflowException("Bash command failed")

        if self.xcom_push_flag:
            return line

    def on_kill(self):
        self.log.info('Sending SIGTERM signal to bash process group')
        os.killpg(os.getpgid(self.sp.pid), signal.SIGTERM)

TemporaryDirectory:创建一个临时的目录,它使用了@contextmanager,生成了一个上下文管理器,因此它能用在with环境里。用contextmanager装饰的函数,要返回一个生成器,并且只能返回一个值

@contextmanager  # 生成一个上下文管理器
def TemporaryDirectory(suffix='', prefix=None, dir=None):
    name = mkdtemp(suffix=suffix, prefix=prefix, dir=dir)  # suffix:后缀  prefix: 前缀
    try:
        yield name    # yield 生成器 仅返回一个值
    finally:
        try:
            shutil.rmtree(name)   # 当with结束之后,删除临时目录
        except OSError as e:
            # ENOENT - no such file or directory
            if e.errno != errno.ENOENT:
                raise e                                              

NamedTemporaryFile: 创建一个临时的文件,它继承了一个类,这个类实现了__enter__, __exit__ 方法,因此能用with
pre_exec: 捕捉信号,并进行信号处理
subprocess.Popen: 具体执行command或者shell脚本

airflow的安装和配置

1.安装
virtualenv airflow
export AIRFLOW_HOME=~/airflow
source airflow/bin/activate
pip install airflow
这个过程时间有点长,airflow安装了很多依赖包,数据库同步工具alembic, orm工具sqlalchemy, flask等
2.初始化数据库
airflow默认的数据库是sqlite,如果你想具体测试airflow的功能的话,你需要指定一个真实的数据库,mysql或者postgresql
airflow initdb

3.启动服务
airflow webserver -p 8080
启动服务之后,你就可以访问127.0.0.1来访问airflow。这时整个网站是没有登录入口的,需要在配置文件里配置才可以看到用户登录界面
4.配置登录界面
airflow配置文件在主目录下,airflow.cfg
找到[webserver]这一项
authenticate = True
auth_backend = airflow.contrib.auth.backends.password_auth
把这两项改完之后,保存配置文件
cd /airflow python
Python 2.7.9 (default, Feb 10 2015, 03:28:08)
Type “help”, “copyright”, “credits” or “license” for more information.

>>> import airflow
>>> from airflow import models, settings
>>> from airflow.contrib.auth.backends.password_auth import PasswordUser
>>> user = PasswordUser(models.User())
>>> user.username = 'new_user_name'
>>> user.email = 'new_user_email@example.com'
>>> user.password = 'set_the_password'
>>> session = settings.Session()
>>> session.add(user)
>>> session.commit()
>>> session.close()
>>> exit()

重启服务 airflow webserver -p 8080
5.设置一个后端
修改airflow.cfg:
executor = LocalExecutor
sql_alchemy_conn = mysql://username:password@localhost:3306/dbname
初始化数据库:
airflow initdb
6.测试airflow的scheduler
启动scheduler服务: airflow scheduler 如果定时任务还没有运行的话,重启一下服务 airflow webserver -p 8080

airflow之DAGs详解

airflow是一个描述,执行,监控工作流的平台。airflow自带了一些dags,当你启动airflow之后,就可以在网页端看到这些dags,我们也可以自己定以dag。

1.什么是DAGs
DAG是一个有向无环图,它是一个task的集合,并且定义了这些task之间的执行顺序和依赖关系。比如,一个DAG包含A,B,C,D四个任务,A先执行,只有A运行成功后B才能执行,C只有在A,B都成功的基础上才能执行,D不受约束,随时都可以执行。DAG并不关心它的组成任务所做的事情,它的任务是确保他们所做的一切都在适当的时间,或以正确的顺序进行,或者正确处理任何意外的问题。

2.什么是operators
DAG定义了一个工作流,operators定义了工作流中的每一task具体做什么事情。一个operator定义工作流中一个task,每个operator是独立执行的,不需要和其他的operator共享信息。它们可以分别在不同的机器上执行。
如果你真的需要在两个operator之间共享信息,可以使用airflow提供的Xcom功能。

airflow目前有一下几种operator:
BashOperator – executes a bash command
PythonOperator – calls an arbitrary Python function
EmailOperator – sends an email
HTTPOperator – sends an HTTP request
SqlOperator – executes a SQL command
Sensor – waits for a certain time, file, database row, S3 key, etc…

3.写一个简单的DAG
first_dag.py

# -*- coding:utf-8 -*-
import airflow
import datetime
from builtins import range
from airflow.operators.bash_operator import BashOperator
from airflow.models import DAG

args = {
    'owner': 'test',
    'start_date': airflow.utils.dates.days_ago(1)
}   # 默认参数

dag = DAG(
    dag_id='first_dag',
    default_args=args,
    schedule_interval='*/1 * * * *',
    dagrun_timeout=datetime.timedelta(minutes=60)
)  # 创建一个DAG实例
dag.sync_to_db()     # 将dag写到数据库中

run_last = BashOperator(task_id='run_last', bash_command='echo 1', dag=dag)   # 定义一个operator,这个operator不做任何操作

for i in range(3):
    op = BashOperator(task_id='task_run_%s'%i, bash_command='ls -l', dag=dag)  # 执行 ls -l 命令
    op.set_downstream(run_last) # set_downstream set_upstream 定义operator的关系

if __name__ == "__main__":
    dag.cli()

icode 快速入门教程

简介

icode是基于gerrit的二次开发,底层的服务由gerrit提供,icode旨在解决使用gerrit过程中的痛点,优化整个开发流程,打通整体流程,包括:代码托管、持续集成、自动化运维,前端部分使用react + mobx + antd搭建,服务端使用python作为连接gerrit与前端的桥梁。

登录

icode使用gerrit账号进行登录,目前还没有开通注册功能,所以新同事可以找运维人员开通gerrit账号来使用icode。

WechatIMG48

创建代码库

点击创建代码库的按钮会出现创建代码库的弹窗,在输入框中输入需要的信息就可以直接创建对应的代码库。WechatIMG50

分支管理

点击进入项目之后便可看到项目完整的分支信息,包括:分知名、备注、创建时间、分支状态、领先落后数量等。

WechatIMG51

新建分支

点击新建分支,会出现弹窗,在输入框中填写具体的信息就可以创建新的分支,分支名默认自动生成,且不允许修改格式。

WechatIMG52

克隆代码库&切换分支

申请分支之后,我们可以先把代码库克隆到本地。

WechatIMG53

ps:想要执行这段命令需要先把ssh-key添加到gerrit账号中。

地址:http://gerrit.dev.aixuexi.com/#/settings/ssh-keys

克隆到本地之后,我们需要切换到我们新生请的分支。

使用:git checkout –track origin/[你的分支] 指令来切换到对应的远程分支,并建立本地分支与其关联。

提交代码

icode为每次提交都添加的评审,且校验了change-id,所以我们提交评审的时候需要使用hook生成并携带change-id,建议添加gpush别名来简化提交。

bash -s << '_EOF_'
git config --global alias.gpush '!f() { : push ; r=$1; [[ -z $r ]] && r=origin; b=$2; t=$(awk "{ print \$2 }" $(git rev-parse --git-dir)/HEAD); t=${t#refs/heads/}; [[ -z $b ]] && b=$t; cmd="git push $r HEAD:refs/for/$b%topic=$t"; echo $cmd; echo; $cmd; }; f'
_EOF_

代码评审

提交评审之后,在icode上实时的看到评审列表,点击评审主题就可以跳转到评审页面。

WechatIMG54

在线预览

点击文件菜单可以在线预览代码库结构以及文件内容。

WechatIMG55

提交历史

点击提交历史,可以查看每一次的提交记录,并且可以查看提交内容的diff记录。

WechatIMG56

WechatIMG57

收藏代码

WechatIMG49

持续集成

点击持续集成菜单,会弹出新的页面进入持续集成平台,在持续集成平台中,我们提供了编译、送测、发布、合并回主干的功能。

  • 编译:点击编译按钮可以编译当前分支的代码,产出服务器上需要的压缩包。
  • 送测:点击送测按钮,可以选择对应的QA,系统会自动发送邮件来提醒对应的QA人员本次提测的代码库地址和分支号。
  • 发布:测试通过之后,点击发布会把压缩包发布到产品库中等待上线操作。
  • 合并回主干:当上线结束之后,点击合并回主干,分支代码会自动合并回master分支,并且删除当前分支,分支的生命周期走到尽头。

WechatIMG58 WechatIMG59 WechatIMG60 WechatIMG62 WechatIMG63

阿里代码规范插件-来自阿里的神秘力量

hello 大家好~

作为程序猿,深知代码规范的重要性,其好处就不用说了,易读,易维护,代码跑起来也有效率、爽快。

不管是使用什么IDE,其自身都会集成一些规范检查,不过基本都是纯英文的,我相信大多人都不怎么会去看的,说句打脸的话,我也不怎么看,,手动捂脸。。

最近阿里的大神们推出了一款代码规范检查插件,试用了下,效果不错,今个做一简单推荐介绍。

GITHUB:https://github.com/alibaba/p3c

官方说明:https://mp.weixin.qq.com/s/IbibsXlWHlM59kfXJqRvZA#rd

首先,怎么安装,so easy~

打开我们的Idea ,选择 File – Settings – Plugins – Browse repositories

然后搜索 Alibaba

安装

搜索出来的第一个插件就是,然后在右边红框会有安装按钮,我这个已经是装过了,装完以后重启Idea即可。

然后,怎么使用嘞。

使用1

可以在类、包以及整个项目上使用,点击右键,就会看到红框位置有一个编码规约扫描,是它是它就是它,大胆的点击它,然后就是见证奇迹的时刻。

使用3 使用2

我这个是跑了整个项目,它的检查结果分为三个级别:简单理解就是崩溃>严重>重要,咱们优先处理等级高的问题,比如下面这个

使用3

点进来以后详细的代码位置都有,然后有没有注意到上边有个神奇的按钮,为代码添加大括号!别犹豫,点它!瞬间当前类下所有没加大括号的全有了有没有,神不神奇,厉不厉害

使用4

这简直就是真·良心功能啊。

另外,它还有一个实时检查的功能,代码有波浪线的时候,鼠标放上去,就会有提示了。

这款强大的插件还有更多神奇的功能,我也刚刚开始使用,还需要大家一起去发现,去研究,插件终究只是辅助

心中有规范、处处成规范

什么时候我们写出的代码,任何主流规范检查都检查不出大问题,岂不是一大快事~

少年,还等什么,快试试吧!

财务系统-axxBank和marginCall项目上线小总结

财务对账一期,在各方势力催促下,匆匆忙忙上线了,上线之后,第一天cat监控显示有4个接口超过了300ms,最高请求能达到1秒多,爆炸!

微信截图_20170930155822
接下来,查找原因,4个接口4条业务线逐一排查,首先走Debug先从sql入手,对于新增的表 没有及时建立索引的,增加索引保证sql最优化,接下来走单元测试打印运行时间,看代码块响应时间,优化代码 !
这次调优印象最深的有两点:

1、有个业务方法,在for循环中加了sql查询,这个错误太明显,

微信图片_20170930155009
二话不说修复代码将所需3张表的数据一次性加载到内存中,在内存中进行各项封装,时间从180ms优化到6ms,鼓掌!~\(≧▽≦)/~
2、另一个错误是在频繁使用一个三级分类查询中,使用了google guava缓存,有一个方法未写正确,导致每次都重新查询加载到缓存中,怎么避免这种失误很简单,debug断点看是否第一次加载进去,第二次从缓存中直接取到,还是不能太相信自己,直接测试功能就认为没问题
经过一些修改之后,使用勇勇的gatling压测,4个接口平均响应时间已经低于40ms

本次小总结:

1、sql优化,养成每个sql在写代码阶段就自己进行性能调优监控的习惯
2、单元测试,不能只是简单的测试功能是否正常,打印出响应时间,好坏立刻见效
3、上述两条只是基本的编程习惯,有了上述两个之后,适当地使用缓存,上线之前进行压测,都是避免不必要问题的措施啦~

江湖路远,下期再见~

微信图片_20170930155456

接口测试考虑事项

在集成测试中首先是确定需要测试模块,集成是将多个模块集合在一起工作,模块与模块之间肯定有工作的接口,你就需要研究一个模块输入输出,研究多个模块的输入输出,构造你如何测试这多个模块输入输出的关系。——-查找各模块的输入输出及关系,编写用例

接口测试主要考虑的问题:

1.各个模块连接集成起来的时候,穿越模块接口的数据会不会丢失; —–确定数据完整

2.各个子功能组合起来,能否达到预期要求的父功能; ——集合后,达到需求目标

3.一个模块的功能是否对另一个模块的功能产生不利影响; ——集成后,不影响相关模块功能

4.全局数据结构是否有问题; ——集成后,保证系统数据的正确性

5.单个模块的误差积累起来,是否会放大,从而达到不可接受的程度。 ——-集成后,确保误差不影响系统功能及性能

Service层接口测试,大致有三种测试类型:接口逻辑测试、出错测试、路径测试

接口逻辑测试,对开发人员输写的JavaDoc进行测试,后根据JavaDoc来编写测试用例(一般情况下JavaDoc需要包含前提条件,业务逻辑,输入参数,输出值的描述),在接口逻辑测试中主要是根据所描述的业务逻辑,进行用例的设计,主要目标是测试在正常输入的情况下能得出正确的结果,测试用例的设计方法跟黑盒测试差不多,主要运用等价类,边界值两种方法。

出错测试,做了接口逻辑测试后,可以正常使用了。为了保证数据的安全,及程序在异常情况的逻辑正确性,因此需要测试出错测试。出错测试主要考虑:空值输入(如当传入一个对象参数时,需进行NULL值的参数)、参数属性的测试(如输入一个未赋值参数)、异常的测试(制造一些异常的测试场景,测试的异常描述是否清晰)

路径测试,经过了上述处理后,单个的接口服务已经得到了保证,但是在业务流中是否满足了业务需求其实还是没有得到保证,路径测试的目的就是设计尽可能少的用例,来保证各种业务场景下数据是安全可操作的。