0%

什么是Dubbo?

Dubbo是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的RPC实现服务的输出和输入功能,以及SOA服务治理方案,和spring框架无缝集成。

dubbo作为一个非常好的rpc项目,广泛在国内使用。

官网:http://dubbo.incubator.apache.org/

github地址:https://github.com/apache/incubator-dubbo

Read more »

java字节码被存储在一个叫做类文件的二进制文件。CtClass的对象代表一个类文件。ClassPool是存放CtClass的hash列表,用类名做key,如果CtClass没发现get()会读取一个class建造一个新的类记录到hash表并返回。

1
2
3
4
5
6
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
cc.setSuperclass(pool.get("test.Point"));
byte[] b = cc.toBytecode();
Class aClass = cc.toClass();
cc.writeFile();

如果系统使用多个类装载器,getDefault()只能搜索当前jvm的路径,可能加载不到对象。

1
2
3
4
5
pool.insertClassPath(new ClassClassPath(this.getClass()));


ClassPool pool = ClassPool.getDefault();
pool.insertClassPath("/usr/local/javalib");

甚至你可以使用:

1
2
3
ClassPool pool = ClassPool.getDefault();
ClassPath cp = new URLClassPath("www.javassist.org", 80, "/java/", "org.javassist.");
pool.insertClassPath(cp);
1
2
3
4
5
6
7
8
9
10
ClassPool cp = ClassPool.getDefault();
byte[] b = a byte array;
String name = class name;
cp.insertClassPath(new ByteArrayClassPath(name, b));
CtClass cc = cp.get(name);


ClassPool cp = ClassPool.getDefault();
InputStream ins = an input stream for reading a class file;
CtClass cc = cp.makeClass(ins);

一个新类可以被定义为一个现有的类的一个副本

1
2
3
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.setName("Pair");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

public class Hello {
public void say() {
System.out.println("Hello");
}
}

public class Test {
public static void main(String[] args) throws Exception {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("Hello");
CtMethod m = cc.getDeclaredMethod("say");
m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
Class c = cc.toClass();
Hello h = (Hello)c.newInstance();
h.say();
}
}

关于内省
http://jboss-javassist.github.io/javassist/tutorial/tutorial2.html#intro

$0, $1, $2, … | 参数
$args | 数组参数Object[]
$$ | 所有参数m($$) 相当于m($1,$2,…)
$cflow(…) | cflow variable
$r | 结果类型.
$w | The wrapper type. It is used in a cast expression.
$_ | 结果值
$sig | An array of java.lang.Class objects representing the formal parameter types.
$type | A java.lang.Class object representing the formal result type.
$class | A java.lang.Class object representing the class currently edited.

$0, $1, $2, …

1
2
3
4
class Point {
int x, y;
void move(int dx, int dy) { x += dx; y += dy; }
}
1
2
3
4
5
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
CtMethod m = cc.getDeclaredMethod("move");
m.insertBefore("{ System.out.println($1); System.out.println($2); }");
cc.writeFile();

结果》

1
2
3
4
5
6
7
class Point {
int x, y;
void move(int dx, int dy) {
{ System.out.println(dx); System.out.println(dy); }
x += dx; y += dy;
}
}

$cflow

$w

Integer i = ($w)5;

通过spring的asm

1
2
3
4
5
6
Method[] methods = CarServiceImpl.class.getMethods();
LocalVariableTableParameterNameDiscoverer local=new LocalVariableTableParameterNameDiscoverer();
String[] params=local.getParameterNames( methods[0]);
for(String param: params){
System.out.println(param);
}

通过javassist

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
Class<?> clazz = Class.forName("com.absurd.rick.service.impl.CarServiceImpl");
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(clazz.getName());

Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method mt:declaredMethods) {
String modifier = Modifier.toString(mt.getModifiers());
Class<?> returnType = mt.getReturnType();
String name = mt.getName();
Class<?>[] parameterTypes = mt.getParameterTypes();

System.out.print("\n"+modifier+" "+returnType.getName()+" "+name+" (");


//CtMethod[] declaredMethods1 = cc.getDeclaredMethods();
CtMethod ctm = cc.getDeclaredMethod(name);
MethodInfo methodInfo = ctm.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
LocalVariableAttribute attribute = (LocalVariableAttribute)codeAttribute.getAttribute(LocalVariableAttribute.tag);
int pos = Modifier.isStatic(ctm.getModifiers()) ? 0 : 1;
for (int i=0;i<ctm.getParameterTypes().length;i++) {
System.out.print(parameterTypes[i]+" "+attribute.variableName(i+pos));
if (i<ctm.getParameterTypes().length-1) {
System.out.print(",");
}
}

System.out.print(")");

Class<?>[] exceptionTypes = mt.getExceptionTypes();
if (exceptionTypes.length>0) {
System.out.print(" throws ");
int j=0;
for (Class<?> cl:exceptionTypes) {
System.out.print(cl.getName());
if (j<exceptionTypes.length-1) {
System.out.print(",");
}
j++;
}
}
}

初始化示例

我们先来看下spring如何手动初始化一个对象

1
2
3
4
5
ClassPathResource res = new ClassPathResource("beans.xml");
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(res);
User user=(User) factory.getBean("user");

Paste_Image.png

spring源码解析

所以我们先从DefaultListableBeanFactory开始了解

AbstractBeanFactory

1
2
3
public <T> T getBean(String name, Class<T> requiredType, Object... args) throws BeansException {
return doGetBean(name, requiredType, args, false);
}
1
2
3
protected <T> T doGetBean(final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
throws BeansException {

  • 1.去掉&开头
1
final String beanName = transformedBeanName(name);
1
2
3
protected String transformedBeanName(String name) {
return canonicalName(BeanFactoryUtils.transformedBeanName(name));
}
  • 2.如果是单例且存在,就直接取过来
1
2
3
4
5
6
7
Object bean;
//拿到缓存的单例实例
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
....
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}
  • 3.不存在单例缓存里面却处于创建状态,可能是循环引用,抛出异常
1
2
3
4
5
6
else {
//可能是一个循环引用,因为拿不到bean但是却处于创建状态
if (isPrototypeCurrentlyInCreation(beanName)) {
//Requested bean is currently in creation: Is there an unresolvable circular reference?
throw new BeanCurrentlyInCreationException(beanName);
}
  • 4.在父BeanFactory查找
1
2
3
4
5
6
7
8
9
10
11
12
13
14
			BeanFactory parentBeanFactory = getParentBeanFactory();
//本beanfactory找不到
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
// 找不到拿原始名去找
String nameToLookup = originalBeanName(name);
if (args != null) {
// Delegation to parent with explicit args.
return (T) parentBeanFactory.getBean(nameToLookup, args);
}
else {
// No args -> delegate to standard getBean method.
return parentBeanFactory.getBean(nameToLookup, requiredType);
}
}
Read more »

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<configuration>
<scanIntervalSeconds>1</scanIntervalSeconds>
<reload>automatic</reload>
<stopPort>9966</stopPort>
<stopKey>foo</stopKey>
<contextXml>${project.basedir}/src/main/resources/jetty-context.xml</contextXml>
<connectors>
<connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector">
<port>8080</port>
<maxIdleTime>60000</maxIdleTime>
</connector>
</connectors>
<webAppSourceDirectory>${basedir}/WebRoot</webAppSourceDirectory>
<webAppConfig>
<contextPath>/absurd</contextPath>
</webAppConfig>
</configuration>
</plugin>

关于这部分的参数,详见链接

安装jrebel以及破解见
IDEA破解
jrebel破解

qq 20160909115113
qq 20160909115148

${project.basedir}/src/main/resources/jetty-context.xml

1
2
3
4
5
6
7
8
9

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
<Call name="setAttribute">
<Arg>org.eclipse.jetty.server.webapp.WebInfIncludeJarPattern</Arg>
<Arg>.*/.*jsp-api-[^/]\.jar$|./.*jsp-[^/]\.jar$|./.*taglibs[^/]*\.jar$</Arg>
</Call>
</Configure>

ctrl+shift+f9编译当前class
ctrl+f9编译全部

1.utf8mb4的最低mysql版本支持版本为5.5.3+,若不是,请升级到较新版本。

2.修改mysql配置文件my.cnf(windows为my.ini)
my.cnf一般在etc/mysql/my.cnf位置。找到后请在以下三部分里添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
[client]
default-character-set = utf8mb4

[mysql]
default-character-set = utf8mb4

[mysqld]
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
init_connect='SET NAMES utf8mb4'

在mysql中执行:

1
2
3
4
5
set character_set_client = utf8mb4;
set character_set_connection = utf8mb4;
set character_set_database = utf8mb4;
set character_set_results = utf8mb4;
set character_set_server = utf8mb4;

重启mysql
Linux:service mysql restart

3.修改database、table和column字符集。参考以下语句:

1
2
3
ALTER DATABASE database_name CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
ALTER TABLE table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE table_name MODIFY COLUMN column_name VARCHAR(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ;

查看是否修改成功
SHOW VARIABLES WHERE Variable_name LIKE 'character\_set\_%' OR Variable_name LIKE 'collation%';

4.如果你用的是java服务器,升级或确保你的mysql connector版本高于5.1.13,否则仍然无法使用utf8mb4

5.jdbc的url必须&characterEncoding=utf8

6.备份数据库的时候
mysqldump -uroot -p --default-character-set=utf8mb4 --hex-blob databasename > databasename.sql

注:在navicat里面会看到乱码
可选:

7.解决不兼容问题

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
public class EmojiUtil {

public static String[] ios5emoji ;
public static String[] ios4emoji ;
public static String[] androidnullemoji ;
public static String[] adsbuniemoji;

public static void initios5emoji(String[] i5emj,String[] i4emj,String[] adnullemoji,String[] adsbemoji){
ios5emoji = i5emj;
ios4emoji = i4emj;
androidnullemoji = adnullemoji;
adsbuniemoji = adsbemoji;
}

//在ios上将ios5转换为ios4编码
public static String transToIOS4emoji(String src) {
return StringUtils.replaceEach(src, ios5emoji, ios4emoji);
}
//在ios上将ios4转换为ios5编码
public static String transToIOS5emoji(String src) {
return StringUtils.replaceEach(src, ios4emoji, ios5emoji);
}
//在android上将ios5的表情符替换为空
public static String transToAndroidemojiNull(String src) {
return StringUtils.replaceEach(src, ios5emoji, androidnullemoji);
}

//在android上将ios5的表情符替换为SBUNICODE
public static String transToAndroidemojiSB(String src) {
return StringUtils.replaceEach(src, ios5emoji, adsbuniemoji);
}

//在android上将SBUNICODE的表情符替换为ios5
public static String transSBToIOS5emoji(String src) {
return StringUtils.replaceEach(src, adsbuniemoji, ios5emoji);
}

//eg. param: 0xF0 0x9F 0x8F 0x80
public static String hexstr2String(String hexstr) throws UnsupportedEncodingException{
byte[] b = hexstr2bytes(hexstr);
return new String(b, "UTF-8");
}

//eg. param: E018
public static String sbunicode2utfString(String sbhexstr) throws UnsupportedEncodingException{
byte[] b = sbunicode2utfbytes(sbhexstr);
return new String(b, "UTF-8");
}

//eg. param: 0xF0 0x9F 0x8F 0x80
public static byte[] hexstr2bytes(String hexstr){
String[] hexstrs = hexstr.split(" ");
byte[] b = new byte[hexstrs.length];

for(int i=0;i<hexstrs.length;i++){
b[i] = hexStringToByte(hexstrs[i].substring(2))[0];
}
return b;
}

//eg. param: E018
public static byte[] sbunicode2utfbytes(String sbhexstr) throws UnsupportedEncodingException{
int inthex = Integer.parseInt(sbhexstr, 16);
char[] schar = {(char)inthex};
byte[] b = (new String(schar)).getBytes("UTF-8");
return b;
}

public static byte[] hexStringToByte(String hex) {
int len = (hex.length() / 2);
byte[] result = new byte[len];
char[] achar = hex.toCharArray();
for (int i = 0; i < len; i++) {
int pos = i * 2;
result[i] = (byte) (toByte(achar[pos]) << 4 | toByte(achar[pos + 1]));
}
return result;
}


private static byte toByte(char c) {
byte b = (byte) "0123456789ABCDEF".indexOf(c);
return b;
}


/**
* 将str中的emoji表情转为byte数组
*
* @param str
* @return
*/
public static String resolveToByteFromEmoji(String str) {
Pattern pattern = Pattern
.compile("[^(\u2E80-\u9FFF\\w\\s`~!@#\\$%\\^&\\*\\(\\)_+-?()——=\\[\\]{}\\|;。,、《》”:;“!……’:'\"<,>\\.?/\\\\*)]");
Matcher matcher = pattern.matcher(str);
StringBuffer sb2 = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb2, resolveToByte(matcher.group(0)));
}
matcher.appendTail(sb2);
return sb2.toString();
}

/**
* 将str中的byte数组类型的emoji表情转为正常显示的emoji表情
*
* @param str
* @return
*/
public static String resolveToEmojiFromByte(String str) {
Pattern pattern2 = Pattern.compile("<:([[-]\\d*[,]]+):>");
Matcher matcher2 = pattern2.matcher(str);
StringBuffer sb3 = new StringBuffer();
while (matcher2.find()) {
matcher2.appendReplacement(sb3, resolveToEmoji(matcher2.group(0)));
}
matcher2.appendTail(sb3);
return sb3.toString();
}

private static String resolveToByte(String str) {
byte[] b = str.getBytes();
StringBuffer sb = new StringBuffer();
sb.append("<:");
for (int i = 0; i < b.length; i++) {
if (i < b.length - 1) {
sb.append(Byte.valueOf(b[i]).toString() + ",");
} else {
sb.append(Byte.valueOf(b[i]).toString());
}
}
sb.append(":>");
return sb.toString();
}

private static String resolveToEmoji(String str) {
str = str.replaceAll("<:", "").replaceAll(":>", "");
String[] s = str.split(",");
byte[] b = new byte[s.length];
for (int i = 0; i < s.length; i++) {
b[i] = Byte.valueOf(s[i]);
}
return new String(b);
}

public static void main(String[] args) throws UnsupportedEncodingException {
// TODO Auto-generated method stub
byte[] b1 = {-30,-102,-67}; //ios5 //0xE2 0x9A 0xBD
byte[] b2 = {-18,-128,-104}; //ios4 //"E018"

//-------------------------------------

byte[] b3 = {-16,-97,-113,-128}; //0xF0 0x9F 0x8F 0x80
byte[] b4 = {-18,-112,-86}; //E42A


ios5emoji = new String[]{new String(b1,"utf-8"),new String(b3,"utf-8")};
ios4emoji = new String[]{new String(b2,"utf-8"),new String(b4,"utf-8")};





//测试字符串
byte[] testbytes = {105,111,115,-30,-102,-67,32,36,-18,-128,-104,32,36,-16,-97,-113,-128,32,36,-18,-112,-86};
String tmpstr = new String(testbytes,"utf-8");
System.out.println(tmpstr);


//转成ios4的表情
String ios4str = transToIOS5emoji(tmpstr);
byte[] tmp = ios4str.getBytes();
//System.out.print(new String(tmp,"utf-8"));
for(byte b:tmp){
System.out.print(b);
System.out.print(" ");
}
}

}

另:
如果你不想重启数据库,可以这样做:

1.表必须是utf8mb4

2.连接池
<property name="connectionInitSqls" value="set names utf8mb4;"/>

或者每次插入前执行
set names utf8mb4

原文地址: http://www.infoq.com/cn/author/%E8%AE%B8%E6%99%93%E6%96%8C

这里只是摘录点笔记

坐标的原则

1
2
3
4
5
6
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
<scope>test</scope>
</dependency>

滥用坐标、错用坐标的样例比比皆是,在中央仓库中我们能看到SpringFramework有两种坐标,其一是直接使用springframework作为groupId,如springframework:spring-beans:1.2.6,另一种是用org.springframework作为groupId,如org.springframework:spring-beans:2.5。细心看看,前一种方式显得比较随意,后一种方式则是基于域名衍生出来的,显然后者更合理,因为用户能一眼根据域名联想到其Maven坐标,方便寻找。因此新版本的SpringFramework构件都使用org.springframework作为groupId。由这个例子我们可以看到坐标规划一个原则是基于项目域名衍生。其实很多流行的开源项目都破坏了这个原则,例如JUnit,这是因为Maven社区在最开始接受构件并部署到中央仓库的时候,没有很很严格的限制,而对于这些流行的项目来说,一时间更改坐标会影响大量用户,因此也算是个历史遗留问题了。

还有一个常见的问题是将groupId直接匹配到公司或者组织名称,因为乍一看这是显而易见的。例如组织是zoo.com,有个项目是dog,那有些人就直接使用groupId com.zoo了。如果项目只有一个模块,这是没有什么问题的,但现实世界的项目往往会有很多模块,Maven的一大长处就是通过多模块的方式管理项目。那dog项目可能会有很多模块,我们用坐标的哪个部分来定义模块呢?groupId显然不对,version也不可能是,那只有artifactId。因此要这里有了另外一个原则,用artifactId来定义模块,而不是定义项目。接下来,很显然的,项目就必须用groupId来定义。因此对于dog项目来说,应该使用groupId com.zoo.dog,不仅体现出这是zoo.com下的一个项目,而且可以与该组织下的其他项目如com.zoo.cat区分开来。

除此之外,artifactId的定义也有最佳实践,我们常常可以看到一个项目有很多的模块,例如api,dao,service,web等等。Maven项目在默认情况下生成的构件,其名称不会是基于artifactId,version和packaging生成的,例如api-1.0.jar,dao-1.0.jar等等,他们不会带有groupId的信息,这会造成一个问题,例如当我们把所有这些构件放到Web容器下的时候,你会发现项目dog有api-1.0.jar,项目cat也有api-1.0.jar,这就造成了冲突。更坏的情况是,dog项目有api-1.0.jar,cat项目有api-2.0.jar,其实两者没什么关系,可当放在一起的时候,却很容易让人混淆。为了让坐标更加清晰,又出现了一个原则,即在定义artiafctId时也加入项目的信息,例如dog项目的api模块,那就使用artifactId dog-api,其他就是dog-dao,dao-service等等。虽然连字号是不允许出现在Java的包名中的,但Maven没这个限制。现在dog-api-1.0.jar,cat-2.0.jar被放在一起时,就不容易混淆了。

关于坐标,我们还没谈到version,这里不再详述因为读者可以从Maven: The Complete Guide中找到详细的解释,简言之就是使用这样一个格式:

<主版本>.<次版本>.<增量版本>-<限定符>
其中主版本主要表示大型架构变更,次版本主要表示特性的增加,增量版本主要服务于bug修复,而限定符如alpha、beta等等是用来表示里程碑。当然不是每个项目的版本都要用到这些4个部分,根据需要选择性的使用即可。在此基础上Maven还引入了SNAPSHOT的概念,用来表示活动的开发状态,由于不涉及坐标规划,这里不进行详述。不过有点要提醒的是,由于SNAPSHOT的存在,自己显式地在version中使用时间戳字符串其实没有必要。

2.

1
2
3
4
5
6
7
8
9
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>

mvn dependency:analyze
分析依赖

最后,还一些重要的POM内容通常被大多数项目所忽略,这些内容不会影响项目的构建,但能方便信息的沟通,它们包括项目URL,开发者信息,SCM信息,持续集成服务器信息等等,这些信息对于开源项目来说尤其重要。对于那些想了解项目的人来说,这些信息能他们帮助找到想要的信息,基于这些信息生成的Maven站点也更有价值。相关的POM配置很简单,如:

1
2
3
4
5
6
7
8
9
10
11
<project>
<description>...</description>
<url>...</url>
<licenses>...</licenses>
<organization>...</organization>
<developers>...</developers>
<issueManagement>...</issueManagement>
<ciManagement>...</ciManagement>
<mailingLists>...</mailingLists>
<scm>...</scm>
</project>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.springframework</groupId>
<artifactid>spring-beans</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactid>spring-context</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactid>spring-core</artifactId>
<version>2.5</version>
</dependency>

你会在一个项目中使用不同版本的SpringFramework组件么?答案显然是不会。因此这里就没必要重复写三次<version>2.5</version>,使用Maven属性将2.5提取出来如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<properties>
<spring.version>2.5</spring.version>
</properties>
<depencencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactid>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactid>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactid>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
</depencencies>

现在2.5只出现在一个地方,虽然代码稍微长了点,但重复消失了,日后升级依赖版本的时候,只需要修改一处,而且也能避免漏掉升级某个依赖。

读者可能已经非常熟悉这个例子了,我这里再啰嗦一遍是为了给后面做铺垫,多模块POM重构的目的和该例一样,也是为了消除重复,模块越多,潜在的重复就越多,重构就越有必要。

dependencyManagement只会影响现有依赖的配置,但不会引入依赖。例如我们可以在父模块中配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependencyManagement>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactid>junit</artifactId>
<version>4.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactid>log4j</artifactId>
<version>1.2.16</version>
</dependency>
</dependencies>
</dependencyManagement>

这段配置不会给任何子模块引入依赖,但如果某个子模块需要使用JUnit和Log4j的时候,我们就可以简化依赖配置成这样:

1
2
3
4
5
6
7
8
<dependency>
<groupId>junit</groupId>
<artifactid>junit</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactid>log4j</artifactId>
</dependency>

你可以把dependencyManagement放到单独的专门用来管理依赖的POM中,然后在需要使用依赖的模块中通过import scope依赖,就可以引入dependencyManagement。例如可以写这样一个用于依赖管理的POM:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.sample</groupId>
<artifactId>sample-dependency-infrastructure</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactid>junit</artifactId>
<version>4.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactid>log4j</artifactId>
<version>1.2.16</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>

然后我就可以通过非继承的方式来引入这段依赖管理配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.juvenxu.sample</groupId>
<artifactid>sample-dependency-infrastructure</artifactId>
<version>1.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependency>
<groupId>junit</groupId>
<artifactid>junit</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactid>log4j</artifactId>
</dependency>

这样,父模块的POM就会非常干净,由专门的packaging为pom的POM来管理依赖,也契合的面向对象设计中的单一职责原则。此外,我们还能够创建多个这样的依赖管理POM,以更细化的方式管理依赖。这种做法与面向对象设计中使用组合而非继承也有点相似的味道。

消除多模块插件配置重复

与dependencyManagement类似的,我们也可以使用pluginManagement元素管理插件。一个常见的用法就是我们希望项目所有模块的使用Maven Compiler Plugin的时候,都使用Java 1.5,以及指定Java源文件编码为UTF-8,这时可以在父模块的POM中如下配置pluginManagement:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.5</source>
<target>1.5</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>

这段配置会被应用到所有子模块的maven-compiler-plugin中,由于Maven内置了maven-compiler-plugin与生命周期的绑定,因此子模块就不再需要任何maven-compiler-plugin的配置了。

持续集成?

  • 只维护一个源码仓库
  • 让构建自行测试
  • 每人每天向主干提交代码
  • 每次提交都应在持续集成机器上构建主干
  • 保持快速的构建
  • 在模拟生产环境中测试
  • 让每个人都能轻易获得最新的可执行文件
  • 每个人都能看到进度
  • 自动化部署

现在,我们希望Maven在integration-test阶段执行所有以IT结尾命名的测试类,配置Maven Surefire Plugin如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.7.2</version>
<executions>
<execution>
<id>run-integration-test</id>
<phase>integration-test</phase>
<goals>
<goal>test</goal>
</goals>
<configuration>
<includes>
<include>**/*IT.java</include>
</includes>
</configuration>
</execution>
</executions>
</plugin>

对应于同样的package生命周期阶段,Maven为jar项目调用了maven-jar-plugin,为war项目调用了maven-war-plugin,换言之,packaging直接影响Maven的构建生命周期。了解这一点非常重要,特别是当你需要自定义打包行为的时候,你就必须知道去配置哪个插件。一个常见的例子就是在打包war项目的时候排除某些web资源文件,这时就应该配置maven-war-plugin如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.1.1</version>
<configuration>
<webResources>
<resource>
<directory>src/main/webapp</directory>
<excludes>
<exclude>**/*.jpg</exclude>
</excludes>
</resource>
</webResources>
</configuration>
</plugin>

本专栏的《坐标规划》一文中曾解释过,一个Maven项目只生成一个主构件,当需要生成其他附属构件的时候,就需要用上classifier。源码包和Javadoc包就是附属构件的极佳例子。它们有着广泛的用途,尤其是源码包,当你使用一个第三方依赖的时候,有时候会希望在IDE中直接进入该依赖的源码查看其实现的细节,如果该依赖将源码包发布到了Maven仓库,那么像Eclipse就能通过m2eclipse插件解析下载源码包并关联到你的项目中,十分方便。由于生成源码包是极其常见的需求,因此Maven官方提供了一个插件来帮助用户完成这个任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.1.2</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>

类似的,生成Javadoc包只需要配置插件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<plugin>          
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.7</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>

为了帮助所有Maven用户更方便的使用Maven中央库中海量的资源,中央仓库的维护者强制要求开源项目提交构件的时候同时提供源码包和Javadoc包。这是个很好的实践,读者也可以尝试在自己所处的公司内部实行,以促进不同项目之间的交流。

可执行CLI包

除了前面提到了常规JAR包、WAR包,源码包和Javadoc包,另一种常被用到的包是在命令行可直接运行的CLI(Command Line)包。默认Maven生成的JAR包只包含了编译生成的.class文件和项目资源文件,而要得到一个可以直接在命令行通过java命令运行的JAR文件,还要满足两个条件:

JAR包中的/META-INF/MANIFEST.MF元数据文件必须包含Main-Class信息。
项目所有的依赖都必须在Classpath中。
Maven有好几个插件能帮助用户完成上述任务,不过用起来最方便的还是maven-shade-plugin,它可以让用户配置Main-Class的值,然后在打包的时候将值填入/META-INF/MANIFEST.MF文件。关于项目的依赖,它很聪明地将依赖JAR文件全部解压后,再将得到的.class文件连同当前项目的.class文件一起合并到最终的CLI包中,这样,在执行CLI JAR文件的时候,所有需要的类就都在Classpath中了。下面是一个配置样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.juvenxu.mavenbook.HelloWorldCli</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>

上述例子中的,我的Main-Class是com.juvenxu.mavenbook.HelloWorldCli,构建完成后,对应于一个常规的hello-world-1.0.jar文件,我还得到了一个hello-world-1.0-cli.jar文件。细心的读者可能已经注意到了,这里用的是cli这个classifier。最后,我可以通过java -jar hello-world-1.0-cli.jar命令运行程序。

自定义格式包

实际的软件项目常常会有更复杂的打包需求,例如我们可能需要为客户提供一份产品的分发包,这个包不仅仅包含项目的字节码文件,还得包含依赖以及相关脚本文件以方便客户解压后就能运行,此外分发包还得包含一些必要的文档。这时项目的源码目录结构大致是这样的:

pom.xml
src/main/java/
src/main/resources/
src/test/java/
src/test/resources/
src/main/scripts/
src/main/assembly/
README.txt
除了基本的pom.xml和一般Maven目录之外,这里还有一个src/main/scripts/目录,该目录会包含一些脚本文件如run.sh和run.bat,src/main/assembly/会包含一个assembly.xml,这是打包的描述文件,稍后介绍,最后的README.txt是份简单的文档。

我们希望最终生成一个zip格式的分发包,它包含如下的一个结构:

bin/
lib/
README.txt
其中bin/目录包含了可执行脚本run.sh和run.bat,lib/目录包含了项目JAR包和所有依赖JAR,README.txt就是前面提到的文档。

描述清楚需求后,我们就要搬出Maven最强大的打包插件:maven-assembly-plugin。它支持各种打包文件格式,包括zip、tar.gz、tar.bz2等等,通过一个打包描述文件(该例中是src/main/assembly.xml),它能够帮助用户选择具体打包哪些文件集合、依赖、模块、和甚至本地仓库文件,每个项的具体打包路径用户也能自由控制。如下就是对应上述需求的打包描述文件src/main/assembly.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
<assembly>
<id>bin</id>
<formats>
<format>zip</format>
</formats>
<dependencySets>
<dependencySet>
<useProjectArtifact>true</useProjectArtifact>
<outputDirectory>lib</outputDirectory>
</dependencySet>
</dependencySets>
<fileSets>
<fileSet>
<outputDirectory>/</outputDirectory>
<includes>
<include>README.txt</include>
</includes>
</fileSet>
<fileSet>
<directory>src/main/scripts</directory>
<outputDirectory>/bin</outputDirectory>
<includes>
<include>run.sh</include>
<include>run.bat</include>
</includes>
</fileSet>
</fileSets>
</assembly>

首先这个assembly.xml文件的id对应了其最终生成文件的classifier。
其次formats定义打包生成的文件格式,这里是zip。因此结合id我们会得到一个名为hello-world-1.0-bin.zip的文件。(假设artifactId为hello-world,version为1.0)
dependencySets用来定义选择依赖并定义最终打包到什么目录,这里我们声明的一个depenencySet默认包含所有所有依赖,而useProjectArtifact表示将项目本身生成的构件也包含在内,最终打包至输出包内的lib路径下(由outputDirectory指定)。
fileSets允许用户通过文件或目录的粒度来控制打包。这里的第一个fileSet打包README.txt文件至包的根目录下,第二个fileSet则将src/main/scripts下的run.sh和run.bat文件打包至输出包的bin目录下。
打包描述文件所支持的配置远超出本文所能覆盖的范围,为了避免读者被过多细节扰乱思维,这里不再展开,读者若有需要可以去参考这份文档。

最后,我们需要配置maven-assembly-plugin使用打包描述文件,并绑定生命周期阶段使其自动执行打包操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.2.1</version>
<configuration>
<descriptors>
<descriptor>src/main/assembly/assembly.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>

运行mvn clean package之后,我们就能在target/目录下得到名为hello-world-1.0-bin.zip的分发包了。

netty3.x

入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void run() {
// Configure the server.
ServerBootstrap bootstrap = new ServerBootstrap(
new NioServerSocketChannelFactory(
Executors.newCachedThreadPool(),
Executors.newCachedThreadPool()));

// Set up the pipeline factory.
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
public ChannelPipeline getPipeline() throws Exception {
return Channels.pipeline(new EchoServerHandler());
}
});
bootstrap.setOption("child.tcpNoDelay", true);
bootstrap.setOption("child.keepAlive", true);
....
// Bind and start to accept incoming connections.
bootstrap.bind(new InetSocketAddress(port));
}


业务代码

1
2
3
4
5
6
7
8
9
public class EchoServerHandler extends SimpleChannelUpstreamHandler {

@Override
public void messageReceived(
ChannelHandlerContext ctx, MessageEvent e) {
// Send back the received message to the remote peer.
e.getChannel().write(e.getMessage());
}
}

精通
手册:http://netty.io/3.7/guide/

netty4.x

入门
服务端

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
   /**
* 服务端监听的端口地址
*/
private static final int portNumber = 7878;

EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup);
b.channel(NioServerSocketChannel.class);
b.childHandler(new HelloServerInitializer());

// 服务器绑定端口监听
ChannelFuture f = b.bind(portNumber).sync();

System.out.println("init server");

// 监听服务器关闭监听
f.channel().closeFuture().sync();

// 可以简写为
/* b.bind(portNumber).sync().channel().closeFuture().sync(); */
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class HelloServerInitializer extends ChannelInitializer<SocketChannel> {

@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();

// 以("\n")为结尾分割的 解码器
pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));

// 字符串解码 和 编码
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());

// 自己的逻辑Handler
pipeline.addLast("handler", new HelloServerHandler());
}
}
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
public class HelloServerHandler extends SimpleChannelInboundHandler<String> {

@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
// 收到消息直接打印输出
System.out.println(ctx.channel().remoteAddress() + " Say : " + msg);

// 返回客户端消息 - 我已经接收到了你的消息
ctx.writeAndFlush("我Received your message "+msg+"!\n");
}

/*
*
* 覆盖 channelActive 方法 在channel被启用的时候触发 (在建立连接的时候)
*
* channelActive 和 channelInActive 在后面的内容中讲述,这里先不做详细的描述
* */
/* @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {

System.out.println("RamoteAddress : " + ctx.channel().remoteAddress() + " active !");

ctx.writeAndFlush( "Welcome to " + InetAddress.getLocalHost().getHostName() + " service!\n");

super.channelActive(ctx);
}*/
}

客户端

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
public static String host = "127.0.0.1";
public static int port = 7878;

EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new HelloClientInitializer());

// 连接服务端
Channel ch = b.connect(host, port).sync().channel();
System.out.println("init client");
// 控制台输入
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
for (;;) {
String line = in.readLine();
if (line == null) {
continue;
}
/*
* 向服务端发送在控制台输入的文本 并用"\r\n"结尾
* 之所以用\r\n结尾 是因为我们在handler中添加了 DelimiterBasedFrameDecoder 帧解码。
* 这个解码器是一个根据\n符号位分隔符的解码器。所以每条消息的最后必须加上\n否则无法识别和解码
* */
ch.writeAndFlush(line + "\r\n");
}
} finally {
// The connection is closed automatically on shutdown.
group.shutdownGracefully();
}

1
2
3
4
5
6
7
8
9
public class HelloClientHandler extends SimpleChannelInboundHandler<String>{

@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
// 收到消息直接打印输出
System.out.println(ctx.channel().remoteAddress() + " Say : " + msg);

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HelloClientInitializer extends ChannelInitializer<SocketChannel>{

@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();

// 以("\n")为结尾分割的 解码器
pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));

// 字符串解码 和 编码
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());

// 自己的逻辑Handler
pipeline.addLast("handler", new HelloClientHandler());
}

}
  • 一个EventLoopGroup包含一个或多个EventLoop
  • 一个EventLoop在它生命周期只和一个Thread绑定
  • 所有EventLoop处理的I/O事件都将在专有的Thread上处理
  • 一个channel在生命周期只注册于一个EventLoop
  • 一个EventLoop可能会分配给一个或多个channel

Channel

  • 线程安全
  • write:数据写到远程结点。数据传给channelpipeline排队直到被冲刷
  • flush:已写数据冲刷到传输底层socket
  • 内置的传输:
  1. NIO/io.netty.channel.socket.nio/基于选择器
  2. Epoll/io.netty.channel.epoll/JNI驱动的epoll和非阻塞IO
  3. OIO/io.netty.channel.socket.oio/java.net基础/阻塞流
  4. Local/io.netty.channel.local/VM内部通过管道进行通信/本地传输
  5. Embedded/io.netty.channel.embedded/Embedded,ChannelHandler不需经过网络

Channel生命周期

  • ChannelUnregistered channel已创建,但还没注册到EventLoop
  • ChannelRegistered 注册到EventLoop
  • ChannelActive channel处于活动状态(已连接到远程结点),可以接收和发送数据
  • ChannelInactive 没有连接到远程结点
    image

ChannelHandler生命周期

  • handlerAdded 当ChannelHandler被添加到一个ChannelPipeline时被调用
  • handlerRemoved 当ChannelHandler从一个ChannelPipeline中移除时被调用
  • exceptionCaught 处理过程中ChannelPipeline中发生错误时被调用

ChannelInboundHandler——处理输入数据和所有类型的状态变化

方法:
image

类型 描述
channelRegistered 当一个Channel注册到EventLoop上,可以处理I/O时被调用
channelUnregistered 当一个Channel从它的EventLoop上解除注册,不再处理I/O时被调用
channelActive 当Channel变成活跃状态时被调用;Channel是连接/绑定、就绪的
channelInactive 当Channel离开活跃状态,不再连接到某个远端时被调用
channelReadComplete 当Channel上的某个读操作完成时被调用
channelRead 当从Channel中读数据时被调用
channelWritabilityChanged 当Channel的可写状态改变时被调用。通过这个方法,用户可以确保写操作不会进行地太快(避免OutOfMemoryError)或者当Channel又变成可写时继续写操作。Channel类的isWritable()方法可以用来检查Channel的可写状态。可写性的阈值可以通过Channel.config().setWriteHighWaterMark()和Channel.config().setWriteLowWaterMark()来设定。
userEventTriggered 因某个POJO穿过ChannelPipeline引发ChannelnboundHandler.fireUserEventTriggered()时被调用

当一个ChannelInboundHandler实现类重写channelRead()方法时,它要负责释放ByteBuf相关的内存

1
2
3
4
5
6
7
public class DiscardHandler extends ChannelInboundHandlerAdapter {  
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//手动释放消息
ReferenceCountUtil.release(msg);
}
}

一个更简单的替代方法就是用SimpleChannelInboundHandler

1
2
3
4
5
6
public class SimpleDiscardHandler extends SimpleChannelInboundHandler<Object> {  
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
//不需要手动释放
}
}
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
public abstract class SimpleChannelInboundHandler<I> extends ChannelInboundHandlerAdapter {
...
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
boolean release = true;
try {
if (acceptInboundMessage(msg)) {
@SuppressWarnings("unchecked")
I imsg = (I) msg;
channelRead0(ctx, imsg);
} else {
release = false;
ctx.fireChannelRead(msg);
}
} finally {
if (autoRelease && release) {
ReferenceCountUtil.release(msg);
}
}
}

protected abstract void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception;
}
...

ChannelOutboundHandler——处理输出数据,可以拦截所有操作

类型 描述
bind(ChannelHandlerContext,SocketAddress,ChannelPromise) 请求绑定Channel到一个本地地址
connect(ChannelHandlerContext, SocketAddress,SocketAddress,ChannelPromise) 请求连接Channel到远端
disconnect(ChannelHandlerContext, ChannelPromise) 请求从远端断开Channel
close(ChannelHandlerContext,ChannelPromise) 请求关闭Channel
deregister(ChannelHandlerContext, ChannelPromise) 请求Channel从它的EventLoop上解除注册
read(ChannelHandlerContext) 请求从Channel中读更多的数据
flush(ChannelHandlerContext) 请求通过Channel刷队列数据到远端
write(ChannelHandlerContext,Object, ChannelPromise) 请求通过Channel写数据到远端

CHANNELPROMISE VS. CHANNELFUTURE
ChannelOutboundHandler的大部分方法都用了一个ChannelPromise输入参数,用于当操作完成时收到通知。ChannelPromise是ChannelFuture的子接口,定义了可写的方法,比如setSuccess(),或者setFailure(),而ChannelFuture则是不可变对象。

ChannelHandler适配器类

image

资源管理

无论何时你对数据操作ChannelInboundHandler.channelRead()或者ChannelOutboundHandler.write(),你需要确保没有资源泄露。也许你还记得上一章我们提到过,Netty采用引用计数来处理ByteBuf池。所以,在你用完一个ByteBuf后,调整引用计数的值是很重要的。

为了帮助你诊断潜在的问题, Netty提供了ResourceLeakDetector类,它通过采样应用程序1%的buffer分配来检查是否有内存泄露。这个过程的开销是很小的。

如果泄露被检测到,会产生类似下面这样的日志消息:

LEAK: ByteBuf.release() was not called before it’s garbage-collected. Enable
advanced leak reporting to find out where the leak occurred. To enable
advanced leak reporting, specify the JVM option
‘-Dio.netty.leakDetectionLevel=ADVANCED’ or call
ResourceLeakDetector.setLevel().

级别 描述
DISABLED 关闭内存泄露检测。 只有在大量测试后,才能用这个级别
SIMPLE 报告默认的1%采样率中发现的任何泄露。这是默认的级别,在大部分情况下适用
ADVANCED 报告发现的泄露和消息的位置。使用默认的采样率。
PARANOID 类似ADVANCED级别,但是每个消息的获取都被检测采样。这对性能有很大影响,只能在调试阶段使用。

用上表中的某个值来配置下面这个Java系统属性,就可以设定内存泄露检测级别:

java -Dio.netty.leakDetectionLevel=ADVANCED

如果你设定这个JVM选项然后重启你的应用,你会看到应用中泄露buffer的最新位置。下面是一个单元测试产生的典型的内存泄露报告:

Running io.netty.handler.codec.xml.XmlFrameDecoderTest
15:03:36.886 [main] ERROR io.netty.util.ResourceLeakDetector - LEAK:
ByteBuf.release() was not called before it’s garbage-collected.
Recent access records: 1

1:io.netty.buffer.AdvancedLeakAwareByteBuf.toString(AdvancedLeakAwareByteBuf.java:697)

io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithXml(XmlFrameDecoderTest.java:157)
io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithTwoMessages(XmlFrameDecoderTest.java:133)

在你实现ChannelInboundHandler.channelRead()或者ChannelOutboundHandler.write()时,你怎样用这个诊断工具来防止内存泄露呢?让我们来看下ChannelRead()操作“消费(consume)”输入数据这个情况:就是说,当前handler没有通过ChannelContext.fireChannelRead()把消息传递到下一个ChannelInboundHandler。下面的代码说明了如何释放这条消息占用的内存。

1
2
3
4
5
6
7
public class DiscardInboundHandler extends ChannelInboundHandlerAdapter {  
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ReferenceCountUtil.release(msg);
}
}

1
2
3
4
5
6
7
public class DiscardOutboundHandler extends ChannelOutboundHandlerAdapter {  
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ReferenceCountUtil.release(msg);
promise.setSuccess();
}
}

重要的是,不仅要释放资源,而且要通知ChannelPromise,否则会出现某个ChannelFutureListener没有被通知到消息已经被处理的情况。

总之,如果一个消息被“消费”或者丢弃,没有送到ChannelPipeline中的下一个ChannelOutboundHandler,用户就要负责调用ReferenceCountUtil.release()。如果消息到达了真正的传输层,在它被写到socket中或者Channel关闭时,会被自动释放(这种情况下用户就不用管了)。

ChannelPipeline接口

https://segmentfault.com/a/1190000007308934
如果你把一个ChannelPipeline看成是一串ChannelHandler实例,拦截穿过Channel的输入输出event,那么就很容易明白这些ChannelHandler的交互是如何构成了一个应用程序数据和事件处理逻辑的核心。

每个新创建的Channel都会分配一个新的ChannelPipeline。这个关系是恒定的;Channel不可以换别ChannelPipeline,也不可以解除掉当前分配的ChannelPipeline。在Netty组件的整个生命周期中这个关系是固定的,不需要开发者采取什么操作。

根据来源,一个event可以被一个ChannelInboundHandler或者ChannelOutboundHandler处理。接下来,通过调用ChannelHandlerContext的方法,它会被转发到下一个同类型的handler。

image

方法名 描述
fireChannelRegistered 调用ChannelPipeline中下一个ChannelInboundHandler的channelRegistered(ChannelHandlerContext)
fireChannelUnregistered 调用ChannelPipeline中下一个ChannelInboundHandler的channelUnRegistered(ChannelHandlerContext)
fireChannelActive 调用ChannelPipeline中下一个ChannelInboundHandler的channelActive(ChannelHandlerContext)
fireChannelInactive 调用ChannelPipeline中下一个ChannelInboundHandler的channelInactive(ChannelHandlerContext)
fireExceptionCaught 调用ChannelPipeline中下一个ChanneHandler的exceptionCaught(ChannelHandlerContext,Throwable)
fireUserEventTriggered 调用ChannelPipeline中下一个ChannelInboundHandler的userEventTriggered(ChannelHandlerContext, Object)
fireChannelRead 调用ChannelPipeline中下一个ChannelInboundHandler的channelRead(ChannelHandlerContext, Object msg)
fireChannelReadComplete 调用ChannelPipeline中下一个ChannelStateHandler的channelReadComplete(ChannelHandlerContext)
方法名 描述
bind 绑定Channel到一个本地地址。这会调用ChannelPipeline中下一个ChannelOutboundHandler的bind(ChannelHandlerContext, SocketAddress, ChannelPromise)
connect 连接Channel到一个远端地址。这会调用ChannelPipeline中下一个ChannelOutboundHandler的connect(ChannelHandlerContext, SocketAddress, ChannelPromise)
disconnect 断开Channel。这会调用ChannelPipeline中下一个ChannelOutboundHandler的disconnect(ChannelHandlerContext, ChannelPromise)
close 关闭Channel。这会调用ChannelPipeline中下一个ChannelOutboundHandler的close(ChannelHandlerContext,ChannelPromise)
deregister Channel从它之前分配的EventLoop上解除注册。这会调用ChannelPipeline中下一个ChannelOutboundHandler的deregister(ChannelHandlerContext, ChannelPromise)
flush 刷所有Channel待写的数据。这会调用ChannelPipeline中下一个ChannelOutboundHandler的flush(ChannelHandlerContext)
write 往Channel写一条消息。这会调用ChannelPipeline中下一个ChannelOutboundHandler的write(ChannelHandlerContext, Object msg, ChannelPromise)   注意:不会写消息到底层的Socket,只是排队等候。如果要写到Socket中,调用flush()或者writeAndFlush()
writeAndFlush 这是先后调用write()和flush()的便捷方法。
read 请求从Channel中读更多的数据。这会调用ChannelPipeline中下一个ChannelOutboundHandler的read(ChannelHandlerContext)

ChannelHandlerContext接口

ChannelHandlerContext代表了一个ChannelHandler和一个ChannelPipeline之间的关系,它在ChannelHandler被添加到ChannelPipeline时被创建。ChannelHandlerContext的主要功能是管理它对应的ChannelHandler和属于同一个ChannelPipeline的其他ChannelHandler之间的交互。

ChannelHandlerContext有很多方法,其中一些方法Channel和ChannelPipeline也有,但是有些区别。如果你在Channel或者ChannelPipeline实例上调用这些方法,它们的调用会穿过整个pipeline。而在ChannelHandlerContext上调用的同样的方法,仅仅从当前ChannelHandler开始,走到pipeline中下一个可以处理这个event的ChannelHandler。

方法名 描述
bind 绑定到给定的SocketAddress,返回一个ChannelFuture
channel 返回绑定的Channel
close 关闭Channel,返回一个ChannelFuture
connect 连接到给定的SocketAddress,返回一个ChannelFuture
deregister 从先前分配的EventExecutor上解除注册,返回一个ChannelFuture
disconnect 从远端断开,返回一个ChannelFuture
executor 返回分发event的EventExecutor
fireChannelActive 触发调用下一个ChannelInboundHandler的channelActive()(已连接)
fireChannelInactive 触发调用下一个ChannelInboundHandler的channelInactive()(断开连接)
fireChannelRead 触发调用下一个ChannelInboundHandler的channelRead()(收到消息)
fireChannelReadComplete 触发channelWritabilityChanged event到下一个ChannelInboundHandler
handler 返回绑定的ChannelHandler
isRemoved 如果绑定的ChannelHandler已从ChannelPipeline中删除,返回true
name 返回本ChannelHandlerContext 实例唯一的名字
Pipeline 返回绑定的ChannelPipeline
read 从Channel读数据到第一个输入buffer;如果成功,触发一条channelRead event,通知handler channelReadComplete
write 通过本ChannelHandlerContext写消息穿过pipeline
在使用ChannelHandlerContext API时,请牢记下面几点:
  • 一个ChannelHandler绑定的ChannelHandlerContext 永远不会改变,所以把它的引用缓存起来是安全的。
  • 像我们在这节刚开始解释过的,ChannelHandlerContext的一些方法和其他类(Channel和ChannelPipeline)的方法名字相似,但是ChannelHandlerContext的方法采用了更短的event传递路程。我们应该尽可能利用这一点来实现最好的性能。

异常出站

1.添加ChannelFutureListener就是为了在ChannelFuture实例上调用addListener(ChannelFutureListener)方法,有两种方法可以做到这个。最常用的方法是在输出操作(比如write())返回的ChannelFuture上调用addListener()。

1
2
3
4
5
6
7
8
9
10
ChannelFuture future = channel.write(...);
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture f) throws Exception {
if (!f.isSuccess()) {
f.cause().printStackTrace();
r.channel().close();
}
}
});

2.添加一个ChannelFutureListener到ChannelPromise,然后将这个ChannelPromise作为参数传入ChannelOutboundHandler方法。下面的代码和前一段代码有相同的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class OutboundExceptionHandler extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
promise.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture f) {
if (!f.isSuccess()) {
f.cause().printStackTrace();
f.channel().close();
}
}
});
}
}

ByteBuf

image

  1. reader index前面的数据是已经读过的数据,这些数据可以扔掉
  2. 从reader index开始,到writer index之前的数据是可读数据
  3. 从writer index开始,为可写区域
    正是因为这样的设计,ByteBuf可以同时读写数据(只要可读区域和可写区域都还有空闲空间),而java.nio.ByteBuffer则必须调用flip()方法才能从写状态切换到读状态。

ByteBufAllocator

1
2
3
4
ByteBufAllocator byteBufAllocator = channel.alloc();
// byteBufAllocator.compositeBuffer();
// byteBufAllocator.buffer();
ByteBuf byteBuf = byteBufAllocator.directBuffer();

image
image

UnpooledByteBufAllocator:不池化,每次调用返回新实例
PooledByteBufAllocator:池化了ByteBuf并最大限度减少内存碎片。使用jemalloc(https://www.cnblogs.com/gaoxing/p/4253833.html)

Unpooled

创建未池化ByteBuf

ByteBufUtil类

  • hexdump 十六进制形式打印ByteBuf内容
  • equals 判断两个ByteBuf相等

Netty系列之Netty高性能之道

Netty系列之Netty线程模型