一个统计Git代码仓库签入代码行数的批量统计脚本

图片来自pixabay.com的hansbenn会员

本文介绍一个统计Git代码仓库签入代码行数的批量统计脚本。

1. 单个Git代码仓库

对单个Git代码仓库,按提交者进行归类统计代码提交行数。

git log --format='%aN' | sort -u | while read name; do echo -en "$name\t"; git log --numstat --author="$name" | awk 'BEGIN{add=0;subs=0;loc=0} {if($1~/^[0-9]+/){add += $1; subs += $2; loc += $1 + $2 }} END {printf "%s\t%s\t%s\n", add, subs, loc }'; done;

请cd到指定代码仓库目录下,然后运行上面的命令,可以看到如下输出,

peipeihh    13594   275 13869

每列的数字意义如下,

  1. 第一列:代码提交人
  2. 第二列:提交的新增代码行数
  3. 第三列:提交的删除代码行数
  4. 第四列:所有提交的代码行数

2. 批量的Git代码仓库

若是有批量的Git代码仓库,则可以使用如下批量分析脚本。

# 1. parse the date of start and end

sinceDate=$1
untilDate=$2

if [ ! $sinceDate ]; then
    sinceDate="1970-01-01"
fi

if [ ! $untilDate ]; then
    untilDate=`date '+%Y-%m-%d'`
fi

echo "the period of analysis: sinceDate = $sinceDate, untilDate = $untilDate"

# 2. prepare the repository folder

repoTmp=$PWD
repoList="$PWD/repoList.txt"
repoLocalDir="$PWD/repoLocal"
repoReportFile="$PWD/tmpReport.txt"
repoFinalReportFile="$PWD/finalReport.txt"

if [ -d "$repoLocalDir" ]; then
    rm -rf $repoLocalDir;
fi
mkdir -p $repoLocalDir;

if [ -f "$repoReportFile" ]; then
    rm $repoReportFile;
fi
touch $repoReportFile;

if [ ! -f "$repoList" ]; then
    echo "Cannot find repoList.txt, please create it and list all the git repositories to be analyzed, a format is as following, "
    echo "```"
    echo "git@gitee.com:pphh/simple-demo.git master"
    echo "```"
    exit 1
fi

# 3. clone the git repo into local and do the investigation

i=1
cat $repoList | while read line
do
    cd $repoLocalDir

    repoDef=( $line )
    repoName=${repoDef[0]}
    repoBranch=${repoDef[1]}

    if [ -z $repoName ]; then
        continue
    fi

    if [ -z $repoBranch ]; then
        repoBranch="master"
    fi

    repoFolder="./$i-repo-$repoBranch"
    let i++
    echo
    echo "start to clone the repository: $repoName, branch = $repoBranch, folder = $repoFolder"
    git clone $repoName -b $repoBranch $repoFolder
    echo "clone is completed!"

    cd $repoFolder
    echo "try to investigate the submission status of the repository: $repoName, branch = $repoBranch"
    git log --format='%aN' | sort -u | while read name; do git log --numstat --author="$name" --since="$sinceDate" --until="$untilDate" | awk 'BEGIN {add=0;subs=0;all=0} {if($1~/^[0-9]+/){add += $1; subs += $2; all += $1 + $2 }} END {printf "'$name'\t%s\t%s\t%s\n", add, subs, all }' >> $repoReportFile; done;

done

# 4. merge the results

cat $repoReportFile | awk '{ newLines[$1]+=$2;deleteLines[$1]+=$3;all[$1]+=$4 } END {for (i in all) print i,newLines[i],deleteLines[i],all[i];}' > $repoFinalReportFile
rm $repoReportFile
echo
echo "name\tnew-code-lines\tdelete-code-lines\tall"
cat $repoFinalReportFile

演示步骤,

  1. 下载上面的脚本,并放到一个目录下,脚本命名为 analyzeGitRepo.sh。
  2. 在同一个目录下,创建repoList.txt文件,文件中列出所有需要分析的代码仓库,格式样例如下,
git@gitee.com:pphh/simple-demo.git master
git@gitee.com:pphh/blog.git master
  1. 运行脚本,命令格式如下
sh ./analyzeGitRepo.sh "2020-01-01" "2021-07-20"

一个输出结果如下,

% sh ./analyzeGitRepo.sh "2020-01-01" "2021-07-20"
the period of analysis: sinceDate = 2020-01-01, untilDate = 2021-07-20

start to clone the repository: git@gitee.com:pphh/simple-demo.git, branch = master, folder = ./1-repo-master
Cloning into './1-repo-master'...
remote: Enumerating objects: 1182, done.
remote: Counting objects: 100% (279/279), done.
remote: Compressing objects: 100% (195/195), done.
remote: Total 1182 (delta 66), reused 0 (delta 0), pack-reused 903
Receiving objects: 100% (1182/1182), 306.24 KiB | 1.76 MiB/s, done.
Resolving deltas: 100% (267/267), done.
clone is completed!
try to investigate the submission status of the repository: git@gitee.com:pphh/simple-demo.git, branch = master

start to clone the repository: git@gitee.com:pphh/blog.git, branch = master, folder = ./2-repo-master
Cloning into './2-repo-master'...
remote: Enumerating objects: 357, done.
remote: Counting objects: 100% (6/6), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 357 (delta 2), reused 0 (delta 0), pack-reused 351
Receiving objects: 100% (357/357), 2.16 MiB | 1.82 MiB/s, done.
Resolving deltas: 100% (88/88), done.
clone is completed!
try to investigate the submission status of the repository: git@gitee.com:pphh/blog.git, branch = master

name    new-code-lines  delete-code-lines   all
peipeihh 1586 0 1586
huangyh 0 0 0

分析的报告同时也输出到了目录下的finalReport.txt文件。

3. 演示脚本

见如下代码仓库,
- https://gitee.com/pphh/simple-demo/tree/master/demo-gitrepo-codeline-analysis

Java Agent中使用fastjson导致宿主应用出现找不到类的异常问题

图片来自pixabay.com的designerpoint会员

fastjson是一个目前应用广泛、高性能的Java JSON开发类库,本文记录一个在Java Agent中由于引用了fastjson,进而导致宿主应用(sprint boot)在启动过程中出现找不到类的问题。

1. 问题现象

最近在进行Java Agent的相关开发工作,某天发现在其中一个spring boot应用中报如下异常(在对宿主应用加载了Java Agent情况下),

[main] ERROR org.springframework.boot.SpringApplication 858 | Application run failed
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'testHttpController': Unsatisfied dependency expressed through field 'xxxHttpClientManager'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'xxxHttpClientManager' defined in URL [jar:file:/xxx/xxxHttpClientManager.class]: Instantiation of bean failed; nested exception is java.lang.NoClassDefFoundError: org/springframework/http/converter/GenericHttpMessageConverter

Caused by: java.lang.NoClassDefFoundError: org/springframework/http/converter/GenericHttpMessageConverter
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
    at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:411)
    at org.springframework.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:151)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at com.xxx.http.client.xxxHttpClientManager.<>(xxxHttpClientManager.java:63)

若不加载Java Agent,单独启动Spring Boot应用则能够正常运行,只要加载Java Agent就会报如上错误,除此之外,日志中并无其它的特别错误报告。

通过上述日志,分析相关代码,获悉到如下的执行顺序,

  1. Spring Boot应用启动。
  2. Spring Boot应用通过BeanInitializer (spring web 5.1.x)尝试初始化Bean。
  3. Spring Boot应用注入 @Autowired testHttpController。
  4. Spring Boot应用注入 @Autowired xxxHttpClientManager。
  5. 在xxxHttpClientManager中有执行new FastJsonHttpMessageConverter,这个类来自fastjson版本1.2.x。

上面FastJsonHttpMessageConverter位于fastjson类库的support扩展类包中,用于支持Spring的http message转化,其继承自Spring Web 5.1.x类库的GenericHttpMessageConverter类。

2. 问题分析和定位

GenericHttpMessageConverter类位于Spring Web 5.1.x类库中,是Spring Web的一个基础类库,按理说只要Spring Boot应用能够正常启动加载,不应该出现找不到这个类的问题。单独启动Spring Boot应用也确实没有这个问题,其出现在加载Java Agent情况下。

一般来说出现ClassNotFoundException异常,应该就是类不在类加载器的加载路径上。通过Arthas查看类加载器信息,

[arthas@9336]$ classloader -t
+-BootstrapClassLoader
+-sun.misc.Launcher$ExtClassLoader@4e50df2e
  +-com.taobao.arthas.agent.ArthasClassloader@23d602e5
  +-sun.misc.Launcher$AppClassLoader@18b4aac2
    +-com.xxx.loader.AgentPluginClassLoader@174f0d06
    +-org.springframework.boot.loader.LaunchedURLClassLoader@47dbb1e2

正常情况下,GenericHttpMessageConverter类应该由LaunchedURLClassLoader类加载器负责加载,出现上面异常,难道是被其它加载器加载了?

继续查看和调试分析,发现有如下异常日志,

Caused by: java.lang.ClassNotFoundException: org.springframework.http.converter.GenericHttpMessageConverter
    at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 60 common frames omitted

根据上面的日志,应用在尝试使用AppClassLoader来加载GenericHttpMessageConverter这个类。于是现在的问题是,为什么应用会尝试走AppClassLoader来加载这个spring mvc类?

由于这个问题和Java Agent有密切相关,只有在加载Java Agent后出现,于是回到Java Agent,打开Agent项目,发现在项目中有引用fastjson类库,

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>${fastjson.version}</version>
</dependency>

从这可以解释相关的问题现象了,如下是整个问题的发生流程,

  1. Java应用启动,通过AppClassLoader加载Java Agent相关类包。
  2. 由于Java Agent引用了fastjson,而Agent是通过AppClassLoader加载,因此AppClassLoader加载了Agent里的fastjson类库。
  3. Spring Web应用开始运行,通过LaunchedURLClassLoader加载Spring相关类,初始化相关Bean。
  4. 应用中有Bean尝试new FastJsonHttpMessageConverter对象,由于Java类加载器双亲委派机制,在寻找FastJsonHttpMessageConverter的类过程中,LaunchedURLClassLoader会让父加载器AppClassLoader先尝试加载,而AppClassLoader中发现了fastjson类库,于是其通过AppClassLoader执行加载,而FastJsonHttpMessageConverter继承自GenericHttpMessageConverter,这个时候AppClassLoader是无法获悉GenericHttpMessageConverter类相关信息,于是报异常。

3. 问题复现

为了更好的了解这个问题,可以下载如下简单的演示项目,

应用中简单地创建一个FastJsonHttpMessageConverter对象,

public static void main(String[] args) {
    System.out.println("this is a simple app...");

    /**
     * 在加载simple-agent的情况下,会报如下异常,
     * java.lang.ClassNotFoundException: org.springframework.http.converter.GenericHttpMessageConverter
     */
    FastJsonHttpMessageConverter fastJsonMsgConverter = new FastJsonHttpMessageConverter();

    System.out.println("fastJsonMessageConverter has been created successfully.");
}

通过mvn clean package编译构建项目代码,会打出如下两个Jar包:simple-app.jar和simple-agent.jar,

+ simple-agent
  + src
  - target
    - simple-agent.jar
+ simple-app
  + src
  - target
    - simple-app.jar

直接运行simple app应用,fastJsonMessageConverter能够正常创建成功。

$ java -jar ./simple-app/target/simple-app.jar

this is a simple app...
fastJsonMessageConverter has been created successfully.

若启动simple app应用时加载Java Agent,则会出现找不到GenericHttpMessageConverter类的异常,问题得到复现。

$ java -javaagent:./simple-agent/target/simple-agent.jar -jar ./simple-app/target/simple-app.jar

this is a demo agent...
this is a simple app...
Exception in thread "main" java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49)
    at org.springframework.boot.loader.Launcher.launch(Launcher.java:108)
    at org.springframework.boot.loader.Launcher.launch(Launcher.java:58)
    at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:65)
Caused by: java.lang.NoClassDefFoundError: org/springframework/http/converter/GenericHttpMessageConverter
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
    at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:411)
    at org.springframework.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:151)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at com.pphh.demo.SimpleApp.main(SimpleApp.java:17)
    ... 8 more
Caused by: java.lang.ClassNotFoundException: org.springframework.http.converter.GenericHttpMessageConverter
    at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 23 more

4. 解决方案

这个问题是由于fastjson中类有对扩展spring mvc类有依赖导致,若能够解决这个相互依赖,则这个问题可以得到解决。在升级的fastjson2中,可以看到已经把这个spring等相关support扩展类单独出一个类包,

<!-- 扩展类 com.alibaba.fastjson2.support.* -->
<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2-extension</artifactId>
    <version>${fastjson2.version}</version>
</dependency>

因此,在Java Agent中可以通过升级到fastjson2引用,即可得到解决。

<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>${fastjson2.version}</version>
</dependency>

5. 相关资料

  1. 高性能的JSON库fastjson
  2. 高性能的JSON库fastjson2

认证、授权、鉴权和权限控制

图片来自pixabay.com的fcja99-5064003会员

本文将对信息安全领域中认证、授权、鉴权和权限控制这四个概念给出相应的定义,并对这个概念之间的相互关系进行梳理。本文给出的概念定义将有助于后续文章中对互联网应用开发用户登录功能的更多讨论。

注:本文讨论的互联网应用开发,主要是指web应用和移动应用的开发。

1. 认证

认证是指根据声明者所特有的识别信息,确认声明者的身份。认证在英文中对应于identification这个单词。

最常见的认证实现方式是通过用户名和密码,但认证方式不限于此。下面都是当前常见到的认证技术,

  • 身份证
  • 用户名和密码
  • 用户手机:手机短信、手机二维码扫描、手势密码
  • 用户的电子邮箱
  • 基于时间序列和用户相关的一次性口令
  • 用户的生物学特征:指纹、语音、眼睛虹膜
  • 用户的大数据识别
  • 等等

为了确认用户的身份,防止伪造,在安全要求高的场合,经常会使用组合认证(或者叫多因素认证),也就是同时使用多个认证方式对用户的身份进行校验。

2. 授权

简单来说,授权一般是指获取用户的委派权限。在英文中对应于authorization这个单词。

在信息安全领域,授权是指资源所有者委派执行者,赋予执行者指定范围的资源操作权限,以便执行者代理执行对资源的相关操作。这里面包含有如下四个重要概念,

  • 资源所有者,拥有资源的所有权利,一般就是资源的拥有者。
  • 资源执行者,被委派去执行资源的相关操作。
  • 操作权限,可以对资源进行的某种操作。
  • 资源,有价值的信息或数据等,受到安全保护。

需要说明的是,资源所有者和执行者可以是自然人,就是普通用户,但不限于自然人。在信息安全领域,资源所有者和执行者,很多时候是应用程序或者机器。比如用户在浏览器上登录一个网站,那么这个浏览器就成为一个执行者,它在用户登录后获取了用户的授权,代表着用户执行各种指令,进行购物、下单、付钱、转账等等操作。

同时,资源所有者和执行者可以是分开的不同实体,也可以是同一个。若是分开的两者,则资源执行者是以资源所有者的代理形式而存在。

授权的实现方式非常多也很广泛,我们常见的银行卡、门禁卡、钥匙、公证书,这些都是现实生活中授权的实现方式。其实现方式主要通过一个共信的媒介完成,这个媒介不可被篡改,不可随意伪造,很多时候需要受保护,防止被窃取。

在互联网应用开发领域,授权所用到的授信媒介主要包括如下几种,

  • 通过web服务器的session机制,一个访问会话保持着用户的授权信息
  • 通过web浏览器的cookie机制,一个网站的cookie保持着用户的授权信息
  • 颁发授权令牌(token),一个合法有效的令牌中保持着用户的授权信息

前面两者常见于web开发,需要有浏览器的支持。

3. 鉴权

鉴权是指对于一个声明者所声明的身份权利,对其所声明的真实性进行鉴别确认的过程。在英文中对应于authentication这个单词。

鉴权主要是对声明者所声明的真实性进行校验。若从授权出发,则会更加容易理解鉴权。授权和鉴权是两个上下游相匹配的关系,先授权,后鉴权。授权和鉴权两个词中的“权”,是同一个概念,就是所委派的权利,在实现上即为授信媒介的表达形式。

因此,鉴权的实现方式是和授权方式有一一对应关系。对授权所颁发授信媒介进行解析,确认其真实性。下面是鉴权的一些实现方式,

  • 门禁卡:通过门禁卡识别器
  • 钥匙:通过相匹配的锁
  • 银行卡:通过银行卡识别器
  • 互联网web开发领域的session/cookie/token:校验session/cookie/token的合法性和有效性

鉴权是一个承上启下的一个环节,上游它接受授权的输出,校验其真实性后,然后获取权限(permission),这个将会为下一步的权限控制做好准备。

4. 权限控制

权限控制是指对可执行的各种操作组合配置为权限列表,然后根据执行者的权限,若其操作在权限范围内,则允许执行,否则禁止。权限控制在英文中对应于access/permission control。

对于权限控制,可以分为两部分进行理解:一个是权限,另一个是控制。权限是抽象的逻辑概念,而控制是具体的实现方式。

先看权限(Permission),这是一个抽象的概念,一般预先定义和配置好,以便控制的具体实现。权限的定义,若简单点,可以直接对应于一个可执行的操作集合。而一般情况下,会有基于角色的方式来定义权限,由角色来封装可执行的操作集合。

若以门禁卡的权限实现为例,上述两种定义方式则可以各自表达为,

  • 这是一个门禁卡,拥有开公司所有的门的权限
  • 这是一个门禁卡,拥有管理员角色的权限,因而可以开公司所有的门

可以看到,权限作为一个抽象的概念,将执行者和可具体执行的操作相分离。

在上文的讨论中,鉴权的输出是权限(Permission)。一旦有了权限,便知道了可执行的操作,接下来就是控制的事情了。

对于控制,是根据执行者的权限,对其所执行的操作进行判断,决定允许或禁止当前操作的执行。现实生活中控制的实现方式,多种多样,

  • 门禁:控制门的开关
  • 自行车锁:控制车轮
  • 互联网web后端服务:控制接口访问,允许或拒绝访问请求

5. 认证、授权、鉴权和权限控制的关系

认证、授权、鉴权和权限控制这四个环节是一个前后依次发生、上下游的关系,

认证-->授权-->鉴权-->权限控制

需要说明的是,这四个环节在有些时候会同时发生。 例如在下面的几个场景,

  • 使用门禁卡开门:认证、授权、鉴权、权限控制四个环节一气呵成,在瞬间同时发生
  • 用户的网站登录:用户在使用用户名和密码进行登录时,认证和授权两个环节一同完成,而鉴权和权限控制则发生在后续的请求访问中,比如在选购物品或支付时。

无论怎样,若从时间顺序方面来看,这四个环节是按时间前后、依次相继发生的关系。

6. 认证和鉴权的关系

这两个概念在很多时候是被混淆最多的概念。被混淆的主要原因,如上文所述,很多时候认证、授权、鉴权和权限控制一同发生,以至于被误解为,认证就是鉴权,鉴权就是认证。

其实两者是不一样的概念,两者都有对身份的确认过程,但是两者的主要区别在于,

  • 认证是确认声明者的本身身份,其作为授权的上游衔接而存在
  • 鉴权是对声明者所声明的真实性进行确认的过程,其作为授权的下游衔接而存在

7. 小结

下面对本文讨论的四个概念用一个表格进行小结,

定义 英文 实现方式
认证 确认声明者的身份 identification 根据声明者独特的识别信息
授权 获取用户的委派权限 authorization 颁发一个授信媒介,不可被篡改,不可伪造,受保护
鉴权 对所声明的权限真实性进行鉴别的过程权限是一个抽象的逻辑概念,定义和配置可执行的操作,而控制是具体的实现方式,通过一定的方式控制操作的允许和禁止 authentication 鉴权和授权是一一对应关系,解析授信媒介,确认其合法性、有效性
权限控制 权限是一个抽象的逻辑概念,定义和配置可执行的操作,而控制是具体的实现方式,通过一定的方式控制操作的允许和禁止 access/permission control 实现方式多样,根据具体情况来实现。

8. 参考文献

  1. 维基百科wiki - Identification
  2. 维基百科wiki - Authorization
  3. 维基百科wiki - Authentication
  4. 维基百科wiki – Access Control