Nico's Blog

兴趣使然的Coder


  • 首页

  • 标签

  • 分类

  • 归档

如何使用Defender优雅的管理权限?

发表于 2018-12-11

¶何为权限管理

权限管理已经不知不觉深入到了我们生活的每一个角落,例如地铁进站的闸机,高速公路上的过路费,停车场的杠杆等等等等。

作为一名开发人员,权限二字对我们的映像更加深刻,无论任何系统,都多多少少与权限管理会沾上关系!什么?你的系统和权限不沾边…好吧,你的代码拉取权限总得有吧!如果还没有的话,你登上掘金看到这篇文章并点了一个赞这个过程就需要好多次权限校验。好了扯远了,我们回归正题,这里使用一张图来简单展示web系统的权限是什么样子:

看完之后,是不是感觉很简单,不错,权限管理并不难,我们只需要将校验这一环节进行开发即可,实现方式也有很多种:

¶方案一:组件封装

我们可以将权限校验的整个过程组件化,例如组件名为AuthComponent,之后再所有需要权限校验的接口对应的方法的开头,调用AuthComponent的verify方法进行校验,根据结果做不同的业务处理!

  • 优点:貌似能达到主要目的,而且非常灵活~
  • 缺点:代码冗余,低内聚,高耦合,不易于维护。

¶方案二:通用处理

在方案一的基础上我们稍加改造,例如使用AOP对需要校验的接口做个切面,在方法执行前我们使用AuthComponent校验一下即可,这样我们的代码就更方便维护了!

  • 优点:弥补了方案一的缺点。
  • 缺点:太过通用化,很难兼容所有的情况,不灵活。

¶方案三:自定义注解

我们将方案一和方案二结合一下,取一灵活,取二通用,我们自定义一个名为@Auth的注解,并且它需要传一个参数,我们这里直径定义为枚举类Level,简单结构如下:

1
public enum Level { LOGIN, ADMIN }

之后我们定义一个注解切面,切向携带@Auth的方法,在方法执行前根据value值的内容,也就是Level的值去做不同的权限管理即可。

  • 优点:灵活可控,通用性还行。
  • 缺点:缺乏组件化,结构零散,不易复用,对于多种场景下需要制定多个切面,不优雅!

¶方案四:使用框架

这是最简单的方法,例如优秀的开源shiro、spring-Security等都可以满足我们的需求,唯一的区别是框架的轻重及使用方式!

¶如何更优雅的管理权限

想必很多同学都在使用第四种方案,也有不少的小伙伴在使用方案三,对于缺点明显的方案一和二,使用的应该很少。

如果我们的服务并不需要那么重的权限管理框架去解决权限问题,又不想不优雅的自定义注解去实现时,我们该怎么办呢?

不妨试试 Defender

¶Defender是什么

defender是一款全面拥抱spring-boot的轻量级,高灵活,高可用的权限框架。如果日常中我们需要更加便捷的对服务增加权限管理,那么defender正合适!

它可以免除我们重复编写自定义注解和切面,只需要调用简单的API即可灵活的指定不同模式的防御网络。

defender提供很多种防御模式,我们可以通过调用简单的api是使用构建不同模式的校验器,从而迅速完成权限的管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@EnableDefender("* org.nico.trap.controller..*.*(..)")
public class DefenderTestConfig {
@Bean
public Defender init(){
return Defender.getInstance()
.registry(Guarder.builder(GuarderType.URI)
.pattern("POST /user/*")
.preventer(caller -> {
return caller.getRequest().getHeader("token") == null
? Result.pass() : Result.notpass("error");
}))
.ready();
}
}

上述代码的作用是对请求符合POST类型且URI前缀为/user/的所有接口做了权限管理。

另外,我们可以使用lambda简单完成权限校验逻辑,又或者使用匿名类实现复杂校验逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Guarder.builder(GuarderType.ANNOTATION)
.pattern("org.nico.trap.controller")
.preventer(new AbstractPreventer() {

@Autowired
private AuthComponent authComponent;

@Override
public Result detection(Caller caller) {
String identity = caller.getAccess().value();
if(! identity.equals(AuthConst.VISITOR)) {
UserBo user = authComponent.getUser();
if(user != null) {
if(identity.equals(AuthConst.ADMIN)){
if(user.getRuleType() == null) {
return Result.notpass(new ResponseVo<>(ResponseCode.ERROR_ON_USER_IDENTITY_MISMATCH));
}
}
}else {
return Result.notpass(new ResponseVo<>(ResponseCode.ERROR_ON_LOGIN_INVALID));
}
}
return Result.pass();
}
})

其中GuarderType.URI和GuarderType.ANNOTATION分别代表URI ANT匹配模式和注解模式,后者是方案三的实现,defender提供简单优雅的api将各种模式的权限校验方式集合在一起。

相比shiro、spring-security,defender显得更加轻便灵活,因为它并没有提供一系列权限更具体的管理实现,而是将校验的实现开放一个接口面向开发者,总体代码大小不超过21k,显然对于轻量级的权限管理,defender更加适合!

defender刚刚起步,如果大家有兴趣可以将之集成在您的开发环境尝一下鲜,项目地址如下

Defender传送门

官方也提供有简单的使用文档

中文文档

English Document

如果您感觉不错,也想参与贡献

如何贡献

深入浅说服务如何以Jar包的方式发布

发表于 2018-11-06
¶序言

笔者前段时间在使用自研框架NF( 传送门 )开发一个自动模板生成工具之后,想将他发布到Linux下,之前一直使用IDE直接run as运行,在遇到发布的时候考虑过发布为war或者jar,在一番抉择之后最终选择了jar(原因是NF自带服务容器,而war为tomcat而生,所以jar更适合NF),所以特意研究了一番如何将普通项目打包成jar发布。

不出意外,最终我成功了,在兴奋之余,希望能够将自己实现的过程及遇到的坑记录下来,让看到有此需求的同学们少走一些弯路!

¶一、何为Jar

JAR 文件格式以流行的 ZIP 文件格式为基础。与 ZIP 文件不同的是,JAR 文件不仅用于压缩和发布,而且还用于部署和封装库、组件和插件程序,并可被像编译器和 JVM 这样的工具直接使用。在 JAR 中包含特殊的文件,如 manifests 和部署描述符,用来指示工具如何处理特定的 JAR。

更多详情通过 传送门 查阅。

¶二、发布服务的几种方案

在web开发完成之后,我们往往想要发布服务到外网服务器中,而外网服务器大多是都是Linux系统,这时我们不能已常规方式直接在IDE中运行,需要特定几种形式去发布。

我们最初最常用的方式就是打包成.war的格式发布到Tomcat的服务容器中,这之后Tomcat会帮助我们解压war包,并加载classes文件夹下的.class到内存中,加载完毕之后,我们的服务就可以在服务器中正常运行,但是.war通常只适合配合Tomcat容器,对于其他服务容器,尤其是自研服务容器来讲,适用性非常差,而Spring Boot率先打破了常规。

Spring Boot采用jar的方式发布,也就是说,我们可以使用Spring Boot提供的maven插件,通过mvn package指令将服务打包成jar的形式发布,这就意味着服务中涉及的所有资源(class文件、依赖jar包、静态资源文件)都将会打包在一个jar包之内,在启动这个层次来讲就异常的简单了,只需要通过java -jar xxxx.jar的方式就可以正常启动服务,这对于我们在自己的服务器中去启动服务来说非常的方便,而Spring Boot是怎么做到这一点的呢?

我们来看一下Spring Boot的pom.xml依赖插件 spring-boot-maven-plugin,全配置如下

1
2
3
4
5
6
7
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>${start-class}</mainClass>
</configuration>
</plugin>

我们通过之前对Jar包的了解已经得知,一个可执行jar的必要因素就是需要一个主函数入口,在上述配置中,我们可以看到很明确的主函数配置<mainClass>${start-class}</mainClass>, 而占位符${start-class}的值就是我们平常开发中用来启动Spring Boot的主函数入口所在类,继续深入spring-boot-maven-plugin中,我们会看到这个插件内部依赖了更多maven自带的原生插件

  • maven-failsafe-plugin
  • maven-jar-plugin
  • maven-surefire-plugin
  • maven-war-plugin
  • maven-resources-plugin
  • maven-shade-plugin

另外少部分插件如下

  • exec-maven-plugin
  • git-commit-id-plugin
  • spring-boot-maven-plugin 自依赖,为了支持自己的插件

从以上插件列表分析,spring-boot-maven-plugin中包含了很多maven原生插件,支持jar和 war的格式发布,我们只站在打包可执行jar的角度来分析以上插件的作用,可以这样理解

  • maven-jar-plugin

设定manifest中的Main-Class参数

  • maven-shade-plugin

用于把多个jar包,打成1个jar包

  • maven-resources-plugin

处理将项目资源(src/main/和 src/test)复制到输出目录的操作

  • maven-surefire-plugin 和 maven-failsafe-plugin

执行测试用例

依赖插件的同时,spring-boot-maven-plugin中还使用<resources>标签来重新定义jar包内部结构。

以上信息是否满足将我们的服务打包成可执行jar呢?我们分析一下,如果达到我们想要的效果,我们需要

  1. 自动配置主函数入口
  2. 静态资源打包
  3. 依赖打包

对比上述插件,我们需要的功能都有,那么我们是否可以使用上方的插件及标签自己写个打包插件试试呢? 当然!这里就不带着大家亲自尝试了,因为下面我要讲另一种Spring Boot没有用到的maven插件进行打包!

¶三、maven-assembly-plugin 插件打包Jar

maven-assembly-plugin是一个超灵活maven项目打包工具,提供默认配置和自定义配置,同时提供Main-Class的配置、静态文件Copy及依赖打包的功能,这里是官方对于这款插件的介绍

The Assembly Plugin for Maven is primarily intended to allow users to aggregate the project output along with its dependencies, modules, site documentation, and other files into a single distributable archive.
Your project can build distribution “assemblies” easily, using one of the convenient, prefabricated assembly descriptors. These descriptors handle many common operations, such as packaging a project’s artifact along with generated documentation into a single zip archive. Alternatively, your project can provide its own descriptor and assume a much higher level of control over how dependencies, modules, file-sets, and individual files are packaged in the assembly.

大概意思就是

Maven的组装插件主要是允许用户将项目输出与它的依赖项、模块、站点文档和其他文件一起集成到一个可分发的归档文件中。您的项目可以使用一种方便的预制组装描述符轻松地构建分布“程序集”。这些描述符处理许多常见的操作,例如将项目的工件连同生成的文档打包到一个zip归档文件中。或者,您的项目可以提供自己的描述符,并对依赖项、模块、文件集和各个文件如何在程序集中打包具有更高的控制级别。

通俗一点,你可以自定义你的项目打包格式,maven-assembly-plugin更像是多个打包插件的集成,并提供多种打包的文件格式,使用方面也很方便,最简单的一个使用如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<plugin>
<artifactId> maven-assembly-plugin </artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>${main-class}</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>

descriptorRefs标签内部可以配置使用官方定制好的打包方式,其中如下可选配置

  • bin
  • jar-with-dependencies
  • src
  • project
    不过官方定制好的有很大的局限性,我们可以将上述改成如下配置,来自定义打包方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>${main-class}</mainClass>
</manifest>
</archive>
<descriptors>
<descriptor>src/main/resource/assembly-fat.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>

可以看出,上述配置去掉了<descriptorRefs>标签,增加了<descriptors>配置,并且子标签中还指向了src/main/resource/assembly-fat.xml这个配置文件,如果你的思路跟着这篇文章走,一定可以猜得到,这个配置文件就是我们自定义打包方式的入口!它的格式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
<id>distribution</id>

<formats>
<format>jar</format>
</formats>

<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<useProjectArtifact>true</useProjectArtifact>
<unpack>true</unpack>
<scope>runtime</scope>
</dependencySet>
</dependencySets>

<includeBaseDirectory>false</includeBaseDirectory>

<fileSets>
<fileSet>
<directory>${basedir}</directory>
<includes>
<include>*.txt</include>
</includes>
<excludes>
<exclude>README.txt</exclude>
<exclude>NOTICE.txt</exclude>
</excludes>
</fileSet>
</fileSets>

<files>
<file>
<source>README.txt</source>
<outputDirectory>/</outputDirectory>
<filtered>true</filtered>
</file>
<file>
<source>NOTICE.txt</source>
<outputDirectory>/</outputDirectory>
<filtered>true</filtered>
</file>
</files>
</assembly>
¶下面是标签的相关介绍
  • <id> 生成文件的后缀,如果有,文件名将会是${artifactId}-${id}.jar
  • <formats>生成文件的格式,可以同时生成多个格式的目标文件
  • dependencySets依赖jar的打包方式
  • includeBaseDirectory是否将项目目录引入进来,如果是True的话,生成的目标文件打开之后将会是项目主目录,我们打包的资源将会被放于这个主目录中(推荐Fasle,因为Main-Class路径通常直接是类路径)
  • <fileSets>引入静态资源的配置(目录级)
  • files引入静态资源的配置(文件级)

以上是最常用的几种标签,更多的配置大家可以查阅官网 传送门

配置完成之后可以通过mvn assembly:assembly或者mvn package指令打包。

介绍完毕,下面会拉取笔者自己用NF框架开发的模板工具来为大家演示一下maven-assembly-plugin在实战中的使用!

¶四、Jar方式发布服务实战

首先是项目结构

1
2
3
4
5
6
7
8
9
Project
│ LICENSE
│ pom.xml =》pom文件
│ README.md
├─src
│ └─main
│ ├─java =》源码目录
│ └─resource =》配置文件目录
└─web =》UI静态资源

从结构中可以看出,我们需要手动配置的打包资源是src/main/resource和web这两个目录,所以我们需要所有配置,将上述两个目录随着我们的.class文件一起打包进jar中,首先在原pom.xml保持不变的基础上插入maven-assembly-plugin插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<archive>
<manifest>
<mainClass>org.nico.ct.CtApplication</mainClass>
</manifest>
</archive>
<descriptors>
<descriptor>src/main/resource/assembly-fat.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>

接下来编辑src/main/resource/assembly-fat.xml文件配置打包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
<id>RELEASE</id>

<formats>
<format>jar</format>
</formats>

<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<useProjectArtifact>true</useProjectArtifact>
<unpack>true</unpack>
<scope>runtime</scope>
</dependencySet>
</dependencySets>

<includeBaseDirectory>false</includeBaseDirectory>

<fileSets>
<fileSet>
<directory>src/main/resource</directory>
<outputDirectory>/</outputDirectory>
<includes>
<include>/**</include>
</includes>
</fileSet>
<fileSet>
<directory>web</directory>
<outputDirectory>/web</outputDirectory>
<includes>
<include>/**</include>
</includes>
</fileSet>
</fileSets>


<files>
<file>
<source>README.md</source>
<outputDirectory>/</outputDirectory>
</file>
</files>

</assembly>

然后运行mvn assembly:assembly,等待maven构建成功

1
2
3
4
5
6
7
8
9
10
11
12
13
...
[INFO] META-INF/ already added, skipping
[INFO] META-INF/MANIFEST.MF already added, skipping
[INFO] org/ already added, skipping
[INFO] org/nico/ already added, skipping
[INFO] META-INF/maven/ already added, skipping
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7.627 s
[INFO] Finished at: 2018-06-30T15:39:47+08:00
[INFO] Final Memory: 24M/269M
[INFO] ------------------------------------------------------------------------

看到BUILD SUCCESS之后,你会发现项目target目录中会有两个jar

  • CoffeeTime-0.0.1-SNAPSHOT.jar
  • CoffeeTime-0.0.1-SNAPSHOT-RELEASE.jar

文件名请忽略,后缀带RELEASE的jar就是maven-assembly-plugin插件生成的jar,解压看下目录

│  assembly-fat.xml
│  cat-mysql-nico.xml
│  cat-mysql.xml
│  cat-redis-nico.xml
│  cat-redis.xml
│  cat.xml
│  logno.properties
│  module-info.class
│  README.md
├─com
│  ├─mchange
│  │ 
│  └─mysql  
├─images
├─META-INF
│  ├─maven
│  │ 
│  └─services
├─net
│  └─sf    
├─org
│  ├─apache
│  │  
│  ├─gjt
│  │ 
│  ├─nico
│  ├─objectweb
│  └─slf4j
│ 
├─redis
│  └─clients
│
└─web
    ├─images
    ├─page 
    ├─plugins  
    ├─script
    ├─style
    ├─video
    └─videojs

路径没问题,我们试下能不能运行,切到jar包所在的目录,执行jar -jar CoffeeTime-0.0.1-SNAPSHOT-RELEASE.jar运行之
这里写图片描述

SUCCESS !

Linux平台Nginx的安装及使用

发表于 2018-11-06

这几天因为需要部署静态资源服务器,所以就找了个vps部署一下Nginx,顺带将vsftpd配置好了,下面就给大家讲一下如何在CentOS上部署Nginx及vsftpd!

如果大家不知道Nginx和vsftpd的用处,请自行百度,这里就不过多介绍,废话少说,进入正题。

¶一、在CentOS上下载Nginx和vsftpd

常用的Nginx下载方式有两种,一种是使用CentOS上自带的yum下载,第二种是从网上下载后在通过make进行编译安装,下面我将会向大家介绍一下这两种安装方式

¶1、yum安装方式

首先我们更新一下yum软件库

1
yum update

然后我们搜索一下yum库关于nginx的rpm包

1
yum list | grep nginx

可以看到下面的列表

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@VM_239_130_centos html]# yum list | grep nginx
nginx-filesystem.noarch 1.10.2-1.el6 @epel
collectd-nginx.x86_64 4.10.9-4.el6 epel
munin-nginx.noarch 2.0.33-1.el6 epel
nginx.x86_64 1.10.2-1.el6 epel
nginx-all-modules.noarch 1.10.2-1.el6 epel
nginx-mod-http-geoip.x86_64 1.10.2-1.el6 epel
nginx-mod-http-image-filter.x86_64 1.10.2-1.el6 epel
nginx-mod-http-perl.x86_64 1.10.2-1.el6 epel
nginx-mod-http-xslt-filter.x86_64 1.10.2-1.el6 epel
nginx-mod-mail.x86_64 1.10.2-1.el6 epel
nginx-mod-stream.x86_64 1.10.2-1.el6 epel
pcp-pmda-nginx.x86_64 3.10.9-9.el6 os

接下来我们选择使用yum安装nginx.x86_64 1.10.2-1.el6 epel

1
yum install nginx

中间会提示我们一次是否确认安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
=======================================================================================================================
Package Arch Version Repository Size
=======================================================================================================================
Installing:
nginx x86_64 1.10.2-1.el6 epel 462 k
Installing for dependencies:
nginx-all-modules noarch 1.10.2-1.el6 epel 7.7 k
nginx-mod-http-geoip x86_64 1.10.2-1.el6 epel 14 k
nginx-mod-http-image-filter x86_64 1.10.2-1.el6 epel 16 k
nginx-mod-http-perl x86_64 1.10.2-1.el6 epel 26 k
nginx-mod-http-xslt-filter x86_64 1.10.2-1.el6 epel 16 k
nginx-mod-mail x86_64 1.10.2-1.el6 epel 43 k
nginx-mod-stream x86_64 1.10.2-1.el6 epel 36 k

Transaction Summary
=======================================================================================================================
Install 8 Package(s)

Total download size: 620 k
Installed size: 1.6 M
Is this ok [y/N]:

输入y继续

1
2
3
4
5
6
7
8
9
10
Installed:
nginx.x86_64 0:1.10.2-1.el6

Dependency Installed:
nginx-all-modules.noarch 0:1.10.2-1.el6 nginx-mod-http-geoip.x86_64 0:1.10.2-1.el6
nginx-mod-http-image-filter.x86_64 0:1.10.2-1.el6 nginx-mod-http-perl.x86_64 0:1.10.2-1.el6
nginx-mod-http-xslt-filter.x86_64 0:1.10.2-1.el6 nginx-mod-mail.x86_64 0:1.10.2-1.el6
nginx-mod-stream.x86_64 0:1.10.2-1.el6

Complete!

安装完毕!!接下来我们来看一下nginx的文件分布

1
whereis nginx
1
2
[root@VM_239_130_centos html]# whereis nginx
nginx: /usr/sbin/nginx /etc/nginx /usr/lib64/nginx /usr/local/nginx /usr/share/nginx /usr/share/man/man3/nginx.3pm.gz /usr/share/man/man8/nginx.8.gz

其中三个文件(夹)比较重要:

路径 作用
/usr/sbin/nginx nginx启动路径
/etc/nginx 存放nginx的配置文件
/usr/share/nginx 默认的nginx资源库

我们首先进入/etc/nginx/中看一下nginx到底有哪些配置文件

1
2
3
4
5
[root@VM_239_130_centos html]# cd /etc/nginx
[root@VM_239_130_centos nginx]# ls
conf.d fastcgi.conf.default koi-utf mime.types.default scgi_params uwsgi_params.default
default.d fastcgi_params koi-win nginx.conf scgi_params.default win-utf
fastcgi.conf fastcgi_params.default mime.types nginx.conf.default uwsgi_params

哇,看到这么多配置文件是不是吓了一跳,其实我们只需要在意nginx.conf就行了,其他的涉及到了再百度,接下来我们进入nginx.conf(这里我们使用vim,系统没有vim的小伙伴可以使用yum install vim进行下载安装)

1
vim nginx.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' #日志格式
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main; #操作成功记录日志

sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;

include /etc/nginx/mime.types;
default_type application/octet-stream;

# Load modular configuration files from the /etc/nginx/conf.d directory.
# See http://nginx.org/en/docs/ngx_core_module.html#include
# for more information.
include /etc/nginx/conf.d/*.conf;
}

上面的配置不要动,我们跟踪到这一行include /etc/nginx/conf.d/*.conf主要作用就是加载更多的配置文件,我们退出vim编辑ESC+:q进入到conf.d文件夹来看一下

1
2
3
[root@VM_239_130_centos nginx]# cd /etc/nginx/conf.d
[root@VM_239_130_centos conf.d]# ls
default.conf default.conf.rpmsave ssl.conf virtual.conf

然后进入default.conf

1
vim default.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#
# The default server
#

server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root /usr/share/nginx/html;

# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;

location / {
}

error_page 404 /404.html;
location = /40x.html {
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
}

}

我们发现这个文件配置的是server{},server的配置就是我们nginx的核心配置,这里先不过多的讲,下面会详细介绍server的配置,但是此时如果我们运行nginx的话将会报错

1
Address family not supported by protocol

我们需要将上面的

1
listen       [::]:80 default_server;

注释掉

1
# listen       [::]:80 default_server;

退出vim编辑,使用/usr/sbin/nginx启动nginx

1
[root@VM_239_130_centos conf.d]# /usr/sbin/nginx

关闭nginx

1
pkill -9 nginx

¶2、编译安装

这里借鉴了腾讯云论坛上的一个帖子: CentOS 7中Nginx1.9.5编译安装教程systemctl启动

先安装gcc 等

1
yum -y install gcc gcc-c++ wget

.然后装一些库

1
yum -y install gcc wget automake autoconf libtool libxml2-devel libxslt-devel perl-devel perl-ExtUtils-Embed pcre-devel openssl-devel

进入默认的软件目录

1
cd /usr/local/src/

下载 nginx软件

1
wget http://nginx.org/download/nginx-1.9.5.tar.gz

如果这个下载太慢可以在这里下载http://nginx.org/download/nginx-1.9.5.tar.gz 下载完后yum -y intall lrzsz 装好上传工具

然后用rz上传到服务器 然后解压文件.

1
tar zxvf nginx-1.9.5.tar.gz

进入 nginx1.9.5的源码 如果想改版本号 可以进入源码目录src/core/nginx.h更改

1
cd nginx-1.9.5/

创建一个nginx目录用来存放运行的临时文件夹

1
mkdir -p /var/cache/nginx

开始configure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
./configure \
--prefix=/usr/local/nginx \
--sbin-path=/usr/sbin/nginx \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--pid-path=/var/run/nginx.pid \
--lock-path=/var/run/nginx.lock \
--http-client-body-temp-path=/var/cache/nginx/client_temp \
--http-proxy-temp-path=/var/cache/nginx/proxy_temp \
--http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
--http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
--http-scgi-temp-path=/var/cache/nginx/scgi_temp \
--user=nobody \
--group=nobody \
--with-pcre \
--with-http_v2_module \
--with-http_ssl_module \
--with-http_realip_module \
--with-http_addition_module \
--with-http_sub_module \
--with-http_dav_module \
--with-http_flv_module \
--with-http_mp4_module \
--with-http_gunzip_module \
--with-http_gzip_static_module \
--with-http_random_index_module \
--with-http_secure_link_module \
--with-http_stub_status_module \
--with-http_auth_request_module \
--with-mail \
--with-mail_ssl_module \
--with-file-aio \
--with-ipv6 \
--with-http_v2_module \
--with-threads \
--with-stream \
--with-stream_ssl_module

接着 编译

1
make

安装

1
make install

启动nginx

1
/usr/sbin/nginx

用ps aux来查看nginx是否启动

1
ps aux|grep nginx

复制代码

然后配置服务

1
vim /usr/lib/systemd/system/nginx.service

按i输入以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Unit]
Description=nginx - high performance web server
Documentation=http://nginx.org/en/docs/
After=network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/var/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t -c /etc/nginx/nginx.conf
ExecStart=/usr/sbin/nginx -c /etc/nginx/nginx.conf
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true

[Install]
WantedBy=multi-user.target

编辑好后保存然后开启开机启动

1
systemctl enable nginx.service

用命令关掉nginx

1
pkill -9 nginx

后面可以用systemctl来操作nginx.service

1
systemctl start nginx.service

这里值得一提的是nginx编译安装后的文件夹和yum安装的文件夹类似,nginx.conf文件都在/etc/nginx下,不过编译安装后的nginx.conf文件内部配置与yum安装略有差异

编译安装后的nginx.conf内部直接配置server,所以编译安装的小伙伴配置server就不用去改/etc/nginx/conf.d下的default.conf文件配置了,直接到nginx.conf文件中改server配置就行了

¶二、Nginx配置详解

1、全局块:配置影响nginx全局的指令。一般有运行nginx服务器的用户组,nginx进程pid存放路径,日志存放路径,配置文件引入,允许生成worker process数等。

2、events块:配置影响nginx服务器或与用户的网络连接。有每个进程的最大连接数,选取哪种事件驱动模型处理连接请求,是否允许同时接受多个网路连接,开启多个网络连接序列化等。

3、http块:可以嵌套多个server,配置代理,缓存,日志定义等绝大多数功能和第三方模块的配置。如文件引入,mime-type定义,日志自定义,是否使用sendfile传输文件,连接超时时间,单连接请求数等。

4、server块:配置虚拟主机的相关参数,一个http中可以有多个server。

5、location块:配置请求的路由,以及各种页面的处理情况。

我们来到yum安装后的/etc/nginx/conf.d/default.conf文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server {
listen 80 default_server;
# listen [::]:80 default_server;
server_name _;
root /usr/share/nginx/html;

# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;

location / {
}

error_page 404 /404.html;
location = /40x.html {
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
}

在server块中

名称 作用
listen nginx监听端口
root nginx资源路径根目录
location url访问的本地资源路径配置(支持通配符)
error_page 跳转报错页面

其中nginx会在资源目录中去找默认的index.html页面,我们进入/usr/share/nginx/html中看一下

1
2
3
4
5
[root@VM_239_130_centos conf.d]# cd /usr/share/nginx/html

[root@VM_239_130_centos html]# ls

404.html 50x.html index.html nginx-logo.png poweredby.png

我们在这个文件夹中创建一个test.html

1
2
3
4
5
[root@VM_239_130_centos html]# touch test.html

[root@VM_239_130_centos html]# ls

404.html 50x.html hello index.html nginx-logo.png poweredby.png test.html world

html页面内容

1
2
3
4
5
6
7
8
<html>
<head>
<title>test</title>
</head>
<body>
<h1>hello world</h1>
</body>
</html>

然后我们在浏览器上去访问test.html

成功~

然后我们修改一下default.conf的配置,增加一个location

1
[root@VM_239_130_centos html]# vim /etc/nginx/conf.d/default.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#
# The default server
#

server {
listen 80 default_server;
# listen [::]:80 default_server;
server_name _;
root /usr/share/nginx/html;

# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;

location / {

}

location /test{
#root /;
}

error_page 404 /404.html;
location = /40x.html {
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
}

}

保存退出然后关闭nginx后重新运行

1
2
[root@VM_239_130_centos html]# pkill -9 nginx
[root@VM_239_130_centos html]# /usr/sbin/nginx

然后我们访问test/test.html

Clipboard Image.png

不料却报错了,找不到网页,这是因为我们新添加了一个location资源路径的配置,他会自动找到root,然后在去找root下面是否有test这个文件夹,有的话就去test文件夹中去找我们访问的test.html,可想而知,我们并没有建立test文件夹

回到 /usr/share/nginx/html,然后建立test文件夹,将test.html移动到test文件夹中

1
2
3
4
5
6
7
[root@VM_239_130_centos html]# mkdir test
[root@VM_239_130_centos html]# mv test.html test/test.html
[root@VM_239_130_centos html]# ls
404.html 50x.html hello index.html nginx-logo.png poweredby.png test world
[root@VM_239_130_centos html]# cd test
[root@VM_239_130_centos test]# ls
test.html

这里要注意一下,我增加的location配置是这样的

1
2
3
location /test{
#root /;
}

如果这个root配置不注释的话将会覆盖server块下的root路径哦,到这里nginx基本配置应该就写完了,大家可以去亲自试一试~

Spring Cloud Config 入门

发表于 2018-11-06

¶一、依赖配置

Maven依赖配置只是配置中心需要的配置,其他配置自加,本文仅以扩展为目标~

¶客户端

Maven配置

1
2
3
4
   <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>

Main

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableConfigServer
public class ConfigApp {

public static void main(String[] args) throws Exception {
SpringApplication.run(ConfigApp.class, args);
}
}

application.yml

1
2
3
4
5
6
7
8
9
10
11
server:
port: 8087
spring:
cloud:
config:
label: master
server:
git:
uri: git配置仓库http地址
username: xxx
password: xxx

¶配置服务端

Maven配置

1
2
3
4
   <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>

application.yml

1
2
3
4
5
6
spring:
cloud:
config:
profile: dev
label: master
uri: http://localhost:8087/

¶二、映射规则

1
2
3
4
5
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.yml
/{label}/{application}-{profile}.properties

application:服务名
profile:环境dev/test/pro
lable:分支

仓库根目录下可以用application.yml作为全局配置,所有服务共享

Jenkins安装及自动部署Maven项目

发表于 2018-11-06

¶一、环境配置

¶OS版本

1
2
[root@VM_0_11_centos /]# rpm -qa | grep centos-release
centos-release-7-4.1708.el7.centos.x86_64

¶Java版本

1
2
3
4
[root@VM_0_11_centos /]# java -version
openjdk version "1.8.0_181"
OpenJDK Runtime Environment (build 1.8.0_181-b13)
OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)

¶Maven版本

1
2
3
4
5
6
7
[root@VM_0_11_centos /]# mvn -v
Apache Maven 3.0.5 (Red Hat 3.0.5-17)
Maven home: /usr/share/maven
Java version: 1.8.0_181, vendor: Oracle Corporation
Java home: /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.181-3.b13.el7_5.x86_64/jre
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "3.10.0-693.el7.x86_64", arch: "amd64", family: "unix"

¶Git版本

1
2
[root@VM_0_11_centos /]# git --version
git version 1.8.3.1

¶二、安装

¶Java安装

1
yum install java-1.8.0-openjdk.x86_64

¶Maven安装

1
yum install maven

¶Git安装

1
yum install git

¶Jenkins安装

rpm包地址:https://pkg.jenkins.io/redhat-stable/

1
rpm -ivh xxx.npm

执行以上指令后即安装完毕,查看一下jenkins所在目录

1
whereis jenkins

控制台输出

1
2
[root@VM_0_11_centos /]# whereis jenkins
jenkins: /usr/lib/jenkins

默认jenkins的配置文件在/etc/sysconfig/jenkins

启动jenkins

1
service jenkins start

关闭jenkins

1
service jenkins stop

¶三、Jenkins部署Maven项目

Jenkins启动只有的默认端口为8080,在保证服务器安全组开放8080端口(或者自己在/etc/sysconfig/jenkins配置文件中修改端口)的前提下,我们可以直接通过浏览器访问Jenkins

1
http://xxxxxx:8080

第一次进入Jenkins会让你走几个步骤

  • 输入管理员密码,密码可以从页面提示的文件中看到
  • 下载默认插件,点击官方推荐的按钮继续往下走
  • 设置账号密码和邮箱地址
  • 登入

一顿操作,我们就来到了Jenkins的Dashboard页面
这里写图片描述

看到这里是不是很激动?别急,Jenkins的新建任务默认是没有Maven选项的,需要自行安装Jenkins的Maven插件!

¶1、安装Jenkins-Maven插件Maven Integration

在首页中点击右侧的系统管理
这里写图片描述
选择管理插件
这里写图片描述
下载Maven Integration
这里写图片描述
点击立即获取之后,等待一分钟左右就下载好了

¶2、配置全局工具

接着我们要做一些工具的配置

再次进入系统管理,点击列表中的全局工具配置
这里写图片描述
配置JDK
这里写图片描述
配置Git
这里写图片描述
配置Maven
这里写图片描述
完毕之后点击SAVE按钮保存

¶3、配置任务信息

经过前面两个步骤,我们的Jenkins可以正式开始工作了,不过在真正为我们提供服务之前,我们需要告诉任务该做什么事情,该怎么做。

例如?我们要构建的项目从何而来?通过什么样的方式或者指令就构建?不废话,开始创建一个新的任务,并且是一个Maven项目
这里写图片描述
点击创建一个新任务进入下一步
这里写图片描述
选择创建一个Maven项目,确定之后,进入任务配置界面

任务信息配置
这里写图片描述
源码库配置,Jenkins要知道如何获取到项目的源码
这里写图片描述
Maven打包指令配置
这里写图片描述
构建策略配置
这里写图片描述
这里写图片描述
构建生命周期配置
这里写图片描述
这一步很关键,在Jenkins帮我们自动拉取代码并且打成jar包之后,我们需要执行shell指令去启动它们,Pre Steps和Post Steps使我们可以在整个周期内灵活的去控制流程~例如写个脚本启动它们!

简单配置之后,点击保存,完成任务配置编辑!

之后的事情就简单了,在首页可以看到一个任务列表,选中自己的任务点击进入
这里写图片描述
点击左侧工具栏的立即构建~
这里写图片描述
Jenkins简单部署完成,看下我的任务构建控制台输出日志
这里写图片描述

看到最下方的成功就代表项目已经成功部署!

Spring Cloud Eureka 使用Nginx做路由网关

发表于 2018-11-06

¶一、起始

在分布式系统的体系中,注册中心的作用及其重要,每个服务可以将自己注册到Eureka中,然后通过心跳包去实时获取注册中心的服务列表,因此达到分布式环境下的Rpc调用及负载。

但是如果使用Eureka做负载均衡,那么将会面临着一个问题:

1
如果要调整负载均衡方案,例如复杂的加权,那么整个系统就要面临着停服的尴尬。

那么我们能不能将负载均衡交给系统之外的中间件处理?本文就拿Spring Cloud环境来举例如何将配置Eureka Client以至将负载的主动权交给Nginx!

¶二、可行性

各服务注册在Eureka上的可识别HOST默认是本机IP,也就是ipAddress这个参数,各个服务从Eureka获取的服务列表大多数情况下是ipAddress:port的搭配形式,而Nginx通常通过server_name来监听80端口去转发各个请求,server_name为域名的形式,那么我们只需要想办法将注册列表的搭配变成如下方式即可:

1
2
ipAddress = account.xt.org
port = 80

也就是说从Eureka上获得的服务列表将会是account.application.api:80,那么这个想法到底能不能做到呢?

我们都知道Spring Cloud提供了Eureka的配置方式,配置域有两个,他们分别是Client和Instance,而关于网络方面的配置大多在后者,其中有两个参数恰好符合我们上述要求:

  • IpAddress:实例IP
  • NonSecurePort:获取该实例应该接收通信的非安全端口,在Spring Cloud中默认为服务的IP

¶三、实现

接下来我们需要将IpAddress配置成我们需要的域名,NonSecurePort配置为80(不配置默认会赋值为当前服务的端口号)

1
2
3
4
5
6
7
8
9
10
11
12
eureka:
client:
serviceUrl:
defaultZone: @serviceUrl@
registryFetchIntervalSeconds: 5
instance:
preferIpAddress: true
ipAddress: account.xt.org
nonSecurePort: 80
lease-renewal-interval-in-seconds: 10
lease-expiration-duration-in-seconds: 90
instance-id: ${spring.cloud.client.ipAddress}:${spring.application.name}:${server.port}

至此,我们的服务间便可以通过域名进行RPC调用,我们可以通过Eureka提供的Rest Operations查看方才修改的服务的注册信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
curl GET http://localhost:8089/eureka/apps/SERVICE-XTOKEN-ACCOUNT
RESPONSE:
<application>
<name>SERVICE-XTOKEN-ACCOUNT</name>
<instance>
<instanceId>192.168.50.200:service-xtoken-account:8094</instanceId>
<hostName>account.xt.org</hostName>
<app>SERVICE-XTOKEN-ACCOUNT</app>
<ipAddr>account.xt.org</ipAddr>
<status>UP</status>
<overriddenstatus>UNKNOWN</overriddenstatus>
<port enabled="true">80</port>
<securePort enabled="false">443</securePort>
<countryId>1</countryId>
<dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">
<name>MyOwn</name>
</dataCenterInfo>
<leaseInfo>
<renewalIntervalInSecs>10</renewalIntervalInSecs>
<durationInSecs>90</durationInSecs>
<registrationTimestamp>1537168990103</registrationTimestamp>
<lastRenewalTimestamp>1537171761036</lastRenewalTimestamp>
<evictionTimestamp>0</evictionTimestamp>
<serviceUpTimestamp>1537168990103</serviceUpTimestamp>
</leaseInfo>
<metadata>
<management.port>8094</management.port>
</metadata>
<homePageUrl>http://account.xt.org:80/</homePageUrl>
<statusPageUrl>http://192.168.50.200:8094/info</statusPageUrl>
<healthCheckUrl>http://192.168.50.200:8094/health</healthCheckUrl>
<vipAddress>service-xtoken-account</vipAddress>
<secureVipAddress>service-xtoken-account</secureVipAddress>
<isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer>
<lastUpdatedTimestamp>1537168990103</lastUpdatedTimestamp>
<lastDirtyTimestamp>1537168990048</lastDirtyTimestamp>
<actionType>ADDED</actionType>
</instance>
</application>

我们发现ipAddr和port两个参数已经变成了我们想要的结果!

不过这时的域名我们的网关是识别不了的,需要我们去本机的Hosts文件中配置一下:

1
2
3
4
## 其他配置
..
..
127.0.0.1 account.xt.org

然后Nginx配置一个代理即可:

1
2
3
4
5
6
7
8
server {
listen 80;
server_name account.xt.org;

location / {
proxy_pass http://127.0.0.1:8094;
}
}

Spring Cloud Gateway深入探究

发表于 2018-11-06

¶Spring Cloud Gateway介绍

废话不多说,看官方文档的介绍

This project provides an API Gateway built on top of the Spring Ecosystem, including: Spring 5, Spring Boot 2 and Project Reactor. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them such as: security, monitoring/metrics, and resiliency.

有道翻译一下:

这个项目提供了一个建在Spring生态系统之上的API网关,包括:Spring 5, Spring Boot 2和project Reactor。Spring Cloud Gateway旨在提供一种简单而有效的方式来路由到api,并为它们提供交叉关注,例如:安全性、监视/度量和弹性。

工作原理如下:

image

Gateway实际上提供了一个在路由上的控制功能,大体包含两个大功能:

  • Route
  • Filter
  • Forward

我们可以通过Route去匹配请求的uri,而每个Router下可以配置一个Filter Chain,我们可以通过Filter去修饰请求和响应及一些类似鉴权等中间动作,通过Forward可以控制重定向和请求转发(实际上Filter也可以做到,只不过这里分开说清晰一点)。在路由控制上,Gateway表现的非常灵活,它有两种配置方式:

  • Yml or Properties File
  • Code

主要名词如下:

  • Route: Route the basic building block of the gateway. It is defined by an ID, a destination URI, a collection of predicates and a collection of filters. A route is matched if aggregate predicate is true.
  • Predicate: This is a Java 8 Function Predicate. The input type is a Spring Framework ServerWebExchange. This allows developers to match on anything from the HTTP request, such as headers or parameters.
  • Filter: These are instances Spring Framework GatewayFilter constructed in with a specific factory. Here, requests and responses can be modified before or after sending the downstream request.

Route 作为Gateway中的基本元素,它有自己的ID、URI和一个Predicate集合、Filter集合。Predicate的作用是判断请求的Uri是否匹配当前的Route,Filter则是匹配通过之后对请求和响应的处理及修饰,那么在Gateway中Route基本结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Gateway{
Route1 {
String id;
String path;
List<Predicate> predicates;
List<Filter> filters;
};
Route2 {
String id;
String path;
List<Predicate> predicates;
List<Filter> filters;
};
...
...
}

Route中的ID作为它的唯一标识,path的作用是正则匹配请求路径,Predicate则是在path匹配的情况下进一步去更加细致的匹配请求路径,如一下例子:

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: before_route
uri: http://example.org
predicates:
- Before=2017-01-20T17:42:47.789-07:00[America/Denver]

只匹配在Jan 20, 2017 17:42 发起的请求

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: http://example.org
predicates:
- Cookie=chocolate, “”

只匹配请求中携带chocolate且值为ch.p的请求

更多例子这里就不一一展开,有兴趣的可以去看下官方文档: 传送门

¶Spring Cloud Gateway 配置

¶Maven

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>2.0.2.BUILD-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/libs-snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>

¶Yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: http://example.org
predicates:
- Cookie=chocolate, ch.p

¶Java Config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Configuration
@RestController
@SpringBootApplication
public class Application {

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/request/**")
.and()
.predicate(new Predicate<ServerWebExchange>() {
@Override
public boolean test(ServerWebExchange t) {
boolean access = t.getRequest().getCookies().get("_9755xjdesxxd_").get(0).getValue().equals("32");
return access;
}
})
.filters(f -> f.stripPrefix(2)
.filter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange);
}
}, 2)
.filter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange);
}
}, 1))
.uri("http://localhost:8080/hello")
).build();
}

@GetMapping("/hello")
public String hello() {
return "hello";
}

public static void main(String[] args) throws ClassNotFoundException {
SpringApplication.run(Application.class, args);
}

}

Yml配置和Java代码配置可以共存,Yml配置的好处是可以直接用自带的一些谓词和Filter,而Java代码配置更加灵活!

¶Spring Cloud Gateway使用

上文已经说过,Gateway支持两种配置,本文主要以Java Config的方式着重讲解,因为官方文档中对于Yml配置的讲解已经足够深入,如有兴趣可以进入传送门

¶Route

一个Route的配置可以足够简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
@RestController
@SpringBootApplication
public class Application1 {

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/user/**")
.uri("http://localhost:8080/hello")
).build();
}

@GetMapping("/hello")
public String hello() {
return "hello";
}

public static void main(String[] args) throws ClassNotFoundException {
SpringApplication.run(Application1.class, args);
}

}

上述Demo定义了一个Route,并且path值为/**,意味着匹配多层uri,如果将path改为/*则意味着只能匹配一层。所以运行上面的程序,那么所有的请求都将被转发到http://localhost:8080/hello

如果uri的配置并没有一个确定的资源,例如http://ip:port,那么/**所匹配的路径将会自动拼装在uri之后:

1
2
request http://当前服务/user/1
forward http://ip:port/user/1

这种方式更适合服务之间的转发,我们可以将uri设置为ip:port也可以设置为xxx.com域名,但是不能自己转发自己的服务,例如

1
2
request http://当前服务/user/1
forward http://当前服务/user/1

这就导致了HTTP 413的错误,无限转发至自己,也就意味着请求死锁,非常致命!最好的方式如下:

我们拟定开两个服务占用的端口分别是8080和8081,我们假如要从8080服务通过/user/**路由匹配转发至8081服务,可以这样做:

1
2
3
4
5
6
7
8
9
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("hello", r -> r
.path("/user/**")
.and()
.uri("http://localhost:8081")
).build();
}

工作跟踪:

1
2
request http://localhost:8080/user/hello
forward http://localhost:8081/user/hello

8081服务接口定义:

1
2
3
4
@GetMapping("/user/hello")
public String hello() {
return "User Say Hello";
}

Reponse Body:

1
User Say Hello

当Gateway代替Zuul时,也就是说在服务间的通讯由Zuul转换成Gateway之后,uri的写法将会变成这个样子:

1
2
3
4
5
6
7
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("user/**")
.uri("lb://USER_SERVER_NAME")
).build();
}

上述代码将user/**所匹配的请求全部转发到USER_SERVER_NAME服务下:

1
2
request /user/1
forward http://USER_SERVER_HOST:USER_SERVER_PORT/user/1

其中lb的含义其实是load balance的意思,我想开发者以lb来区分路由模式可能是负载均衡意味着多服务的环境,因此lb可以表示转发对象从指定的uri转变成了服务!

¶Predicate

Predicate是Java 8+新出的一个库,本身作用是进行逻辑运算,支持种类如下:

  • isEqual
  • and
  • negate
  • or
    另外还有一个方法test(T)用于触发逻辑计算返回一个Boolean类型值。

Gateway使用Predicate来做除path pattern match之外的匹配判断,使用及其简单:

1
2
3
4
5
6
7
8
9
10
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("hello", r -> r
.path("/user/**")
.and()
.predicate(e -> e.getClass() != null)
.uri("http://localhost:8081")
).build();
}

输入的e代表着ServerWebExchange对象,我们可以通过ServerWebExchange获取所有请求相关的信息,例如Cookies和Headers。通过Lambda语法去编写判断逻辑,如果一个Route中所有的Predicate返回的结果都是TRUE则匹配成功,否则匹配失败。

Tp:path和predicate需要使用and链接,也可以使用or链接,分别代表不同的逻辑运算!

¶Filter

Filter的作用类似于Predicate,区别在于,Predicate可以做请求中断,Filter也可以做,Filter可以做Reponse的修饰,Predicate并做不到,也就是说Filter最为最后一道拦截,可以做的事情有很多,例如修改响应报文,增加个Header或者Cookie,甚至修改响应Body,相比之下,Filter更加全能!

1
2
3
4
5
6
7
8
9
10
11
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("hello", r -> r
.path("/user/**")
.and()
.predicate(e -> e.getClass() != null)
.filters(fn -> fn.addResponseHeader("developer", "Nico"))
.uri("http://localhost:8081")
).build();
}

¶Spring Cloud Gateway 工作原理

Gateway是基于Spring MVC之上的网关路由控制器,我们可以直接定位到Spring MVC的org.springframework.web.reactive.DispatcherHandler类,它的handle方法将会处理解析Request之后的ServerWebExchange对象。

进入handle方法,将会使用Flux遍历org.springframework.web.reactive.DispatcherHandler.handlerMappings对ServerWebExchange进行处理,handlerMappings中包含着一下处理器:

1
2
org.springframework.web.reactive.function.server.support.RouterFunctionMapping@247a29b6, org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping@f6449f4, org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping@535c6b8b,
org.springframework.web.reactive.handler.SimpleUrlHandlerMapping@5633e9e

以上只是一个简单请求的处理器,Flux的concatMap方法会将每个处理器的Mono合并成一个Flux,然后调用org.springframework.web.reactive.DispatcherHandler类中的invokeHandler方法开始处理ServerWebExchange,处理完毕之后将紧接着处理返回值,这时使用handleResult方法,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
if (logger.isDebugEnabled()) {
ServerHttpRequest request = exchange.getRequest();
logger.debug("Processing " + request.getMethodValue() + " request for [" + request.getURI() + "]");
}
if (this.handlerMappings == null) {
return Mono.error(HANDLER_NOT_FOUND_EXCEPTION);
}
return Flux.fromIterable(this.handlerMappings)
.concatMap(mapping -> mapping.getHandler(exchange))
.next()
.switchIfEmpty(Mono.error(HANDLER_NOT_FOUND_EXCEPTION))
.flatMap(handler -> invokeHandler(exchange, handler))
.flatMap(result -> handleResult(exchange, result));
}

Gateway起作用的关键在于invokeHandler方法中:

1
2
3
4
5
6
7
8
9
10
private Mono<HandlerResult> invokeHandler(ServerWebExchange exchange, Object handler) {
if (this.handlerAdapters != null) {
for (HandlerAdapter handlerAdapter : this.handlerAdapters) {
if (handlerAdapter.supports(handler)) {
return handlerAdapter.handle(exchange, handler);
}
}
}
return Mono.error(new IllegalStateException("No HandlerAdapter: " + handler));
}

对应的处理器的适配器匹配到本身之后,将会触发适配器的处理方法,每个处理器都会实现一个对应的接口,他们大致都有一个共同的特点:

1
2
3
4
5
public interface Handler {

Mono<Void> handle(ServerWebExchange exchange);

}

每个适配器的处理方法中都会在穿插入适配逻辑代码之后调用处理器的handle方法,Spring Cloud Gateway的所有Handler都在

1
2
3
4
```
org.springframework.cloud.gateway.handler.AsyncPredicate<T>
org.springframework.cloud.gateway.handler.FilteringWebHandler
org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping

可以看到,上述三个处理器分别处理了Filter和Predicate,有兴趣的朋友可以去看一下这些类内部的具体实现,这里就不一一细说。

总的来讲,Spring Cloud Gateway的原理是在服务加载期间读取自己的配置将信息存放在一个容器中,在Spring Mvc 处理请求的时候取出这些信息进行逻辑判断及过滤,根据不同的处理结果触发不同的事件!

而请求转发这一块有兴趣的同学可以去研究一下!

TP: path的匹配其实也是一个Predicate逻辑判断

¶Spring Cloud Gateway 总结

笔者偶然间看到spring-cloud-netflix的issue下的一个回答传送门:

1
2
Lovnx:Zuul 2.0 has opened sourcing,Do you intend to integrate it in some next version?
spencergibb:No. We created spring cloud gateway instead.

这才感觉到Spring Cloud果然霸气,也因此接触到了Spring Cloud Gateway,觉得很有必要学习一下,总体来讲,Spring Cloud Gateway简化了之前过滤器配置的复杂度,也在新的配置方式上增加了微服务的网关配置,可以直接代替掉Zuul,期待着Spring会整出自己的注册中心来。

笔者学艺不精,以上阐述有误的地方,希望批评指出,联系方式:ainililia@163.com

¶相关文档

官方文档
Spring Cloud Gateway 入门
响应式编程之Reactor的关于Flux和Mono概念

使用Arthas监控Java进程

发表于 2018-11-06 | 分类于 DevOps

¶一、Arthas简介

Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。

当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:

  1. 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
  2. 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
  3. 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
  4. 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
  5. 是否有一个全局视角来查看系统的运行状况?
  6. 有什么办法可以监控到JVM的实时运行状态?

Arthas采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。

使用说明可以看下官方的文档https://alibaba.github.io/arthas/

¶二、安装及使用

以下通过CentOS系统举例来安装Arthas

¶1.安装Java环境

首先去Oracle下载JDK,可以通过wget或者curl -O来下载到Linux主机上。

最方便的就是下载一个tar.gz格式的压缩包,然后通过tar -zxf解压,以下是笔者最终存放jdk的物理路径

1
2
[nico@VM_0_17_centos jdk1.8.0_181]$ pwd
/usr/lib/java-1.8.0/jdk1.8.0_181

接下来配置环境变量,通过vim /etc/profile进入编辑操作,并增加以下配置

1
2
3
4
export JAVA_HOME=/usr/lib/java-1.8.0/jdk1.8.0_181
export JRE_HOME=$JAVA_HOME/jre
export CLASSPATH=$JAVA_HOME/lib/
export PATH=$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH

退出并保存,通过执行source /etc/profile立即生效

然后分别执行java和javac验证环境是否生效

¶2.安装Arthas

Arthas非常小巧,官方有一个可直接执行的sh脚本供我们使用,可以通过以下指令下载

1
curl -L https://alibaba.github.io/arthas/install.sh | sh

启动Arthas

1
./as.sh

¶3.安装ElasticSearch

Arthas启动需要存在至少一个及以上的Java进程,这里我们为了方便测试,直接安装ElasticSearch。

和安装JDK的方式类似,我们去官方下载一个tar.gz的压缩包,然后解压

1
2
3
4
[nico@VM_0_17_centos elasticsearch]$ ll
total 95612
drwxr-xr-x 9 nico root 4096 Sep 19 09:03 elasticsearch-6.4.0
-rw-r--r-- 1 nico root 97901357 Aug 23 23:21 elasticsearch-6.4.0.tar.gz

ElasticSearch的不允许root用户启动,所以笔者用的是nico账号,创建账号过程如下

1
2
3
4
5
6
7
adduser nico
passwd nico
#输入密码
#将as.sh和elasticsearch的目录权限赋予nico账户
chown nico as.sh
chown -R nico elasticsearch
su nico

以上指令请分开到对应的目录执行,执行完毕之后我们进入elasticsearch目录下的bin目录中,启动elasticsearch

1
./elasticsearch

¶4.使用Arthas监控ElasticSearch

进入as.sh所在目录,启动Arthas

1
./as.sh
1
2
3
4
[nico@VM_0_17_centos arthas]$ ./as.sh
Arthas script version: 3.0.4
Found existing java process, please choose one and hit RETURN.
* [1]: 19670 org.elasticsearch.bootstrap.Elasticsearch

我们看到,当前只有ElasticSearch一个进程,输入1监控ElasticSearch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[nico@VM_0_17_centos arthas]$ ./as.sh
Arthas script version: 3.0.4
Found existing java process, please choose one and hit RETURN.
* [1]: 19670 org.elasticsearch.bootstrap.Elasticsearch
1
Calculating attach execution time...
Attaching to 19670 using version 3.0.4...

real 0m0.227s
user 0m0.177s
sys 0m0.035s
Attach success.
Connecting to arthas server... current timestamp is 1537320967
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'


wiki: https://alibaba.github.io/arthas
version: 3.0.4
pid: 19670
timestamp: 1537320967728

$

启动成功,Arthas提供一个shell的操作模式来供我们去监控Java进程,具体指令可以看下官方的Wiki

¶5.安装过程可能遇到的问题

(1)遇到报错java.security.AccessControlException: Access Denied

官方解决办法

1
2
3
4
5
6
7
Add the permission in client.policy (for the application client), or in server.policy (for EJB/web modules) for the application that needs to set the property. By default, applications only have “read” permission for properties.

For example, to grant read/write permission for all the files in the codebase directory, add or append the following to client.policy or server.policy:

grant codeBase "file:/.../build/sparc_SunOS/sec/-" {
permission java.util.PropertyPermission "*", "read,write";
};

java.policy所在的目录为JDK所在目录下的相对目录jre/lib/security

1
vim java.policy

尾部增加一行即可

1
permission java.security.AllPermission;

使Mybatis开发变得更加轻松的增强工具 — Ourbatis

发表于 2018-11-06

¶一、Mybatis的不足之处

Mybatis是一款优秀的及其灵活的持久层框架,通过XML配置并映射到Mapper接口为Service层提供基础数据操作入口。

这么优秀的框架竟然还有不足之处?

俗话说人无完人,因为Mybatis实在是太灵活了,灵活到每个Mapper接口都需要定制对应的XML,所以就会引发一些问题。

¶问题一:配置文件繁多

假如一个系统中DB中涉及100张表,我们就需要写100个Mapper接口,还没完,最可怕的是,我们要为这100个Mapper接口定制与之对应的100套XML。而每个Mapper都必不可少的需要增删改查功能,我们就要写100遍增删改查,作为高贵的Java开发工程师,这是不能容忍的,于是Mybatis Generator诞生了,然而又会引发另一个问题!

¶问题二:维护困难

我们使用Mybatis Generator解决了问题一,再多的文件生成就是了,简单粗暴,貌似解决了所有的问题,Mybatis完美了!

不要高兴的太早,在系统刚刚建立起来时,我们使用Mybatis Generator生成了一堆XML,在开发过程中,产品忽然提了一个新的需求,项目经理根据这个需求在某张表中增加或变动了一个字段,这时,我猜你的操作是这样:

  • 1、找到对应表的XML
  • 2、将该XML中自定义的一段标签复制出来,保存在本地
  • 3、使用Mybatis Generator重新生成该表的XML
  • 4、将之覆盖当前的XML
  • 5、将自定义的一段标签再粘贴进新的XML中

在这个过程中,如果我们在第2步时漏复制了一段标签,等整个操作完成之后,又别是一番滋味在心头~

¶问题三:编写XML困难

假如肝不错,问题二也是小CASE,那么问题又来了,我们如何在繁长的XML中去编写和修改我们的XML呢。

当我们打开要编辑的XML,映入眼帘的就是1000多行的XML,其中900行都是通用的增删改查操作,要新增一个标签,我们需要拉至文件底部编写新的数据操作,要更新一个标签,我们需要通过Ctrl + F寻找目标标签再进行修改。

如何避免这些问题呢?

如何让Mybatis增强通用性又不失灵活呢?

¶二、使用Ourbatis辅助Mybatis

Ourbatis是一款Mybatis开发增强工具,小巧简洁,项目地址:

  • Github:https://github.com/ainilili/ourbatis
  • Gitee:https://gitee.com/ainilili/ourbatis
  • Wiki:https://github.com/ainilili/ourbatis/wiki
  • Demo:https://github.com/ainilili/ourbatis-simple

特性:

  • 1、简洁方便,可以让Mybatis无XML化开发。
  • 2、优雅解耦,通用和自定义的SQL标签完全隔离,让维护更加轻松。
  • 3、无侵入性,Mybatis和Ourbatis可同时使用,配置简洁。
  • 4、灵活可控,通用模板可自定义及扩展。
  • 5、部署快捷,只需要一个依赖,两个配置,即可直接运行。
  • 6、多数据源,在多数据源环境下也可以照常使用。

¶关于Ourbatis使用的一个小Demo

环境:

  • Spring Boot 2.0.5.RELEASE
  • Ourbatis 1.0.5
  • JAVA 8
  • Mysql

以Spring Boot 2.0.5.RELEASE版本为例,在可以正常使用Mybatis的项目中,pom.xml添加如下依赖:

1
2
3
4
5
   <dependency>
<groupId>com.smallnico</groupId>
<artifactId>ourbatis-spring-boot-starter</artifactId>
<version>1.0.5</version>
</dependency>

在配置文件中增加一下配置:

1
ourbatis.domain-locations=实体类所在包名

接下来,Mapper接口只需要继承SimpleMapper即可:

1
2
3
import org.nico.ourbatis.domain.User;
public interface UserMapper extends SimpleMapper<User, Integer>{
}

至此,一个使用Ourbatis的简单应用已经部署起来了,之后,你就可以使用一些Ourbatis默认的通用操作方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public T selectById(K key);

public T selectEntity(T condition);

public List<T> selectList(T condition);

public long selectCount(Object condition);

public List<T> selectPage(Page<Object> page);

default PageResult<T> selectPageResult(Page<Object> page){
long total = selectCount(page.getEntity());
List<T> results = null;
if(total > 0) {
results = selectPage(page);
}
return new PageResult<>(total, results);
}

public K selectId(T condition);

public List<K> selectIds(T condition);

public int insert(T entity);

public int insertSelective(T entity);

public int insertBatch(List<T> list);

public int update(T entity);

public int updateSelective(T entity);

public int updateBatch(List<T> list);

public int delete(T condition);

public int deleteById(K key);

public int deleteBatch(List<K> list);

¶Mapper自定义方法

在很多场景中,我们使用以上的自带的通用方法远远不能满足我们的需求,我们往往需要额外扩展新的Mapper方法、XML标签,我们使用了Ourbatis之后该如何实现呢?

首先看一下我们的需求,在上述Demo中,我们在UserMapper中增加一个方法selectNameById:

1
2
3
4
import org.nico.ourbatis.domain.User;
public interface UserMapper extends SimpleMapper<User, Integer>{
public String selectNameById(Integer userId);
}

和Mybatis一样,需要在resources资源目录下新建一个文件夹ourbatis-mappers,然后在其中新建一个XML文件,命名规则为:

1
DomainClassSimpleName + Mapper.xml

其中DomainClassSimpleName就是我们实体类的类名,这里是为User,那么新建的XML名为UserMapper.xml。

1
2
3
src/main/resources
- ourbatis-mappers
- UserMapper.xml

之后,打开UserMapper.xml,开始编写Mapper中selectNameById方法对应的标签:

1
2
3
<select id="selectNameById" resultType="java.lang.String">
select name from user where id = #{userId}
</select>

注意,整个文件中只需要写标签就行了,其他的什么都不需要,这是为什么呢?深入之后你就会明白,这里先不多说!

接下来,就没有接下来了,可以直接使用selectNameById方法了。

¶深入了解Ourbatis

ourbatis 流程图

当服务启动的时候,Ourbatis首先会扫描ourbatis.domain-locations配置包下的所有实体类,将之映射为与之对应的表结构数据:
ourbatis Mapping

然后通过ourbatis.xml的渲染,生成一个又一个的XML文件,最后将之重新Build到Mybatis容器中!

整个过程分为两个核心点:

  • 1、映射实体类为元数据
  • 2、使用ourbatis.xml渲染元数据为XML文件

我会一一介绍之~

¶映射实体类为元数据

在映射时,我们要根据自己数据库字段命名的风格去调整映射规则,就需要在第1个核心点中去做处理,Ourbatis使用包装器来完成:

1
2
3
public interface Wrapper<T> {
public String wrapping(T value);
}

对于需要映射的字段,如表名和表字段名,它们都将会经过一个包装器链条的处理之后再投入到ourbatis.xml中做渲染,这样就使得我们可以自定义包装器出更换映射的字段格式,具体方式可以参考官方Wiki:Wrapper包装器

¶使用ourbatis.xml渲染元数据为XML文件

而在于第2个核心点中,Ourbatis通过自定义标签做模板渲染,我们可以先看一下官方默认的ourbatis.xml内部构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="@{mapperClassName}">
<resultMap id="BaseResultMap" type="@{domainClassName}">
<ourbatis:foreach list="primaryColumns" var="elem">
<id column="@{elem.jdbcName}" property="@{elem.javaName}" />
</ourbatis:foreach>
<ourbatis:foreach list="normalColumns" var="elem">
<result column="@{elem.jdbcName}" property="@{elem.javaName}" />
</ourbatis:foreach>
</resultMap>

<sql id="Base_Column_List">
<ourbatis:foreach list="allColumns" var="elem"
split=",">
`@{elem.jdbcName}`
</ourbatis:foreach>
</sql>

<select id="selectById" parameterType="java.lang.Object"
resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from @{tableName}
where 1 = 1
<ourbatis:foreach list="primaryColumns" var="elem">
and `@{elem.jdbcName}` = #{@{elem.javaName}}
</ourbatis:foreach>
</select>

<select id="selectEntity" parameterType="@{domainClassName}"
resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from @{tableName}
where 1 = 1
<ourbatis:foreach list="allColumns" var="elem">
<if test="@{elem.javaName} != null">
and `@{elem.jdbcName}` = #{@{elem.javaName}}
</if>
</ourbatis:foreach>
limit 1
</select>

<select id="selectCount" parameterType="@{domainClassName}"
resultType="long">
select count(0)
from @{tableName}
where 1 = 1
<ourbatis:foreach list="allColumns" var="elem">
<if test="@{elem.javaName} != null">
and `@{elem.jdbcName}` = #{@{elem.javaName}}
</if>
</ourbatis:foreach>
limit 1
</select>

<select id="selectPage"
parameterType="org.nico.ourbatis.entity.Page"
resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from @{tableName}
where 1 = 1
<if test="entity != null">
<ourbatis:foreach list="allColumns" var="elem">
<if test="entity.@{elem.javaName} != null">
and `@{elem.jdbcName}` = #{entity.@{elem.javaName}}
</if>
</ourbatis:foreach>
</if>
<if test="orderBy != null">
order by ${orderBy}
</if>
<if test="start != null and end != null">
limit ${start},${end}
</if>
</select>

<select id="selectList" parameterType="@{domainClassName}"
resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from @{tableName}
where 1 = 1
<ourbatis:foreach list="allColumns" var="elem">
<if test="@{elem.javaName} != null">
and `@{elem.jdbcName}` = #{@{elem.javaName}}
</if>
</ourbatis:foreach>
</select>

<select id="selectId" parameterType="@{domainClassName}"
resultType="java.lang.Object">
select
<ourbatis:foreach list="primaryColumns" var="elem"
split=",">
`@{elem.jdbcName}`
</ourbatis:foreach>
from @{tableName}
where 1 = 1
<ourbatis:foreach list="allColumns" var="elem">
<if test="@{elem.javaName} != null">
and `@{elem.jdbcName}` = #{@{elem.javaName}}
</if>
</ourbatis:foreach>
limit 1
</select>

<select id="selectIds" parameterType="@{domainClassName}"
resultType="java.lang.Object">
select
<ourbatis:foreach list="primaryColumns" var="elem"
split=",">
`@{elem.jdbcName}`
</ourbatis:foreach>
from @{tableName}
where 1 = 1
<ourbatis:foreach list="normalColumns" var="elem">
<if test="@{elem.javaName} != null">
and `@{elem.jdbcName}` = #{@{elem.javaName}}
</if>
</ourbatis:foreach>
</select>

<delete id="deleteById" parameterType="java.lang.Object">
delete
from @{tableName}
where 1=1
<ourbatis:foreach list="primaryColumns" var="elem">
and `@{elem.jdbcName}` = #{@{elem.javaName}}
</ourbatis:foreach>
</delete>

<insert id="insert" keyProperty="@{primaryColumns.0.jdbcName}"
useGeneratedKeys="true" parameterType="@{domainClassName}">
insert into @{tableName}
(
<include refid="Base_Column_List" />
)
values (
<ourbatis:foreach list="allColumns" var="elem"
split=",">
#{@{elem.javaName}}
</ourbatis:foreach>
)
</insert>

<insert id="insertSelective"
keyProperty="@{primaryColumns.0.jdbcName}" useGeneratedKeys="true"
parameterType="@{domainClassName}">
insert into @{tableName}
(
<ourbatis:foreach list="primaryColumns" var="elem"
split=",">
`@{elem.jdbcName}`
</ourbatis:foreach>
<ourbatis:foreach list="normalColumns" var="elem">
<if test="@{elem.javaName} != null">
,`@{elem.jdbcName}`
</if>
</ourbatis:foreach>
)
values (
<ourbatis:foreach list="primaryColumns" var="elem">
#{@{elem.javaName}}
</ourbatis:foreach>
<ourbatis:foreach list="normalColumns" var="elem">
<if test="@{elem.javaName} != null">
, #{@{elem.javaName}}
</if>
</ourbatis:foreach>
)
</insert>

<insert id="insertBatch"
keyProperty="@{primaryColumns.0.jdbcName}" useGeneratedKeys="true"
parameterType="java.util.List">
insert into @{tableName}
(
<include refid="Base_Column_List" />
)
values
<foreach collection="list" index="index" item="item"
separator=",">
(
<ourbatis:foreach list="allColumns" var="elem"
split=",">
#{item.@{elem.javaName}}
</ourbatis:foreach>
)
</foreach>
</insert>

<update id="update" parameterType="@{domainClassName}">
update @{tableName}
<set>
<ourbatis:foreach list="normalColumns" var="elem"
split=",">
`@{elem.jdbcName}` = #{@{elem.javaName}}
</ourbatis:foreach>
</set>
where 1=1
<ourbatis:foreach list="primaryColumns" var="elem">
and `@{elem.jdbcName}` = #{@{elem.javaName}}
</ourbatis:foreach>
</update>

<update id="updateSelective" parameterType="@{domainClassName}">
update @{tableName}
<set>
<ourbatis:foreach list="primaryColumns" var="elem"
split=",">
`@{elem.jdbcName}` = #{@{elem.javaName}}
</ourbatis:foreach>
<ourbatis:foreach list="normalColumns" var="elem">
<if test="@{elem.javaName} != null">
,`@{elem.jdbcName}` = #{@{elem.javaName}}
</if>
</ourbatis:foreach>
</set>
where 1=1
<ourbatis:foreach list="primaryColumns" var="elem">
and `@{elem.jdbcName}` = #{@{elem.javaName}}
</ourbatis:foreach>
</update>

<update id="updateBatch" parameterType="java.util.List">
<foreach collection="list" index="index" item="item"
separator=";">
update @{tableName}
<set>
<ourbatis:foreach list="normalColumns" var="elem"
split=",">
`@{elem.jdbcName}` = #{item.@{elem.javaName}}
</ourbatis:foreach>
</set>
where 1=1
<ourbatis:foreach list="primaryColumns" var="elem">
and `@{elem.jdbcName}` = #{item.@{elem.javaName}}
</ourbatis:foreach>
</foreach>
</update>

<delete id="deleteBatch" parameterType="java.util.List">
delete from @{tableName} where @{primaryColumns.0.jdbcName} in
<foreach close=")" collection="list" index="index" item="item"
open="(" separator=",">
#{item}
</foreach>
</delete>

<delete id="delete" parameterType="@{domainClassName}">
delete from @{tableName} where 1 = 1
<ourbatis:foreach list="allColumns" var="elem">
<if test="@{elem.javaName} != null">
and `@{elem.jdbcName}` = #{@{elem.javaName}}
</if>
</ourbatis:foreach>
</delete>

<ourbatis:ref path="classpath:ourbatis-mappers/@{domainSimpleClassName}Mapper.xml" />
</mapper>

可以看出来,ourbatis.xml内容类似于原生的Mybatis的XML,不同的是,有两个陌生的标签:

  • ourbatis:foreach 对元数据中的列表进行循环渲染
  • ourbatis:ref 引入外界文件内容

这是Ourbatis中独有的标签,Ourbatis也提供着对应的入口让我们去自定义标签:

1
2
3
4
5
6
7
8
9
Class: org.nico.ourbatis.Ourbatis
Field:
public static final Map<String, AssistAdapter> ASSIST_ADAPTERS = new HashMap<String, AssistAdapter>(){
private static final long serialVersionUID = 1L;
{
put("ourbatis:foreach", new ForeachAdapter());
put("ourbatis:ref", new RefAdapter());
}
};

我们可以修改org.nico.ourbatis.Ourbatis类中的静态参数ASSIST_ADAPTERS去删除、更新和添加自定义标签,需要实现一个标签适配器,我们可以看一下最简单的RefAdapter适配器的实现:

1
2
3
4
5
6
7
8
public class RefAdapter extends AssistAdapter{
@Override
public String adapter(Map<String, Object> datas, NoelRender render, Document document) {
String path = render.rending(datas, document.getParameter("path"), "domainSimpleClassName");
String result = StreamUtils.convertToString(path.replaceAll("classpath:", ""));
return result == null ? "" : result.trim();
}
}

Ourbatis中只定义了上述两个自定义标签已足够满足需求,通过foreach标签,将元数据中的集合遍历渲染,通过ref标签引入外界资源,也就是我们之前所说的对Mapper接口中方法的扩展!

1
<ourbatis:ref path="classpath:ourbatis-mappers/@{domainSimpleClassName}Mapper.xml" />

其中的path就是当前项目classpath路径的相对路径,而@{domainSimpleClassName}就代表着实体类的类名,更多的系统参数可以参考Wiki:元数据映射

通过这种模板渲染的机制,Ourbatis是相当灵活的,我们不仅可以通过引入外部文件进行扩展,当我们需要添加或修改通用方法时,我们可以可以自定义ourbatis.xml的内容,如何做到呢?复制一份将之放在资源目录下就可以了!

看到这里,相信大家已经知道Ourbatis的基本原理已经使用方式,我就再次不多说了,更多细节可以去官方Wiki中阅读:Ourbatis Wiki

12
Nico

Nico

我是Nico,梦想是做一名兴趣使然的码农,于是我一步步变强,后来我成为了光头

19 日志
4 分类
7 标签
GitHub
友链
  • Nico's笔记
  • Abby's博客
  • 低调小熊猫
  • ZhangSan_Plus'博客
© 2022 小Nico