一、知识点
Bean后处理器允许在初始化回调方法前后进行额外的Bean处理。Bean后处理器的主要特性是逐个处理IoC容器中的所有Bean实例,而不只是单个Bean实例。一般来说,Bean后处理器用于检查Bean属性的有效性,或根据特定条件修改Bean属性。
Bean后处理器的基本要求是实现BeanPostProcessor接口。通过实现postProcessBeforeInitialization() 和 postProcessAfterInitialization() 方法,可以在初始化回调方法前后处理所有Bean。Spring将在调用初始化回调方法前后向这两个方法传递每个Bean实例,步骤如下:
(1)构造函数或者工厂方法创建Bean实例
(2)为Bean属性设值和Bean引用
(3)调用感知接口中定义的设值方法
(4)将Bean实例传递给每个Bean后处理器中的postProcessBeforeInitialization()方法
(5)调用初始化回调方法
(6)将Bean实例传递给每个Bean后处理器中的postProcessAfterInitialization()方法
(7)Bean准备就绪,可以使用
(8)容器关闭时,调用销毁回调函数
使用Bean工厂作为IoC容器时,Bean后处理器只能编程注册,即通过addBeanPostProcessor方法注册。如果使用应用程序上下文,只需要在Bean配置文件中声明一个处理器实例,它就会自动注册。
二、代码示例
StorageConfig标记接口
package com.codeproject.jackie.springrecipesnote.springadvancedioc; /** * 标记接口,让Bean后处理器区分应检查的Bean * @author jackie * */ public interface StorageConfig { public String getPath(); }Cashier类
package com.codeproject.jackie.springrecipesnote.springadvancedioc; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.Date; import org.springframework.beans.factory.BeanNameAware; /** * Cashier类实现BeanNameAware感知接口和,StorageConfig标记接口 * @author jackie * */ public class Cashier implements BeanNameAware, StorageConfig { private String name; private BufferedWriter writer; private String path; public void setPath(String path) { this.path = path; } public void openFile() throws IOException { File file = new File(path, name + ".txt"); FileWriter fw = new FileWriter(file, true); writer = new BufferedWriter(fw); } public void checkout(ShoppingCart cart) throws IOException { double total = 0; for (Product product : cart.getItems()) { total += product.getPrice(); } writer.write(new Date() + "\t" + total + "\r\n"); writer.flush(); } public void closeFile() throws IOException { writer.close(); } @Override public String getPath() { return path; } @Override public void setBeanName(String beanName) { this.name = beanName; } }PathCheckingBeanPostProcessor自定义Bean后处理器
package com.codeproject.jackie.springrecipesnote.springadvancedioc; import java.io.File; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; /** * 通用的组件,确保文件在打开之前存在 * @author jackie * */ public class PathCheckingBeanPostProcessor implements BeanPostProcessor { /** * 在文件打开之前进行路径检查 * 须返回所处理的Bean的一个实例。 */ @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { // 如果Bean实现了StorageConfig接口,检查路径是否存在 if (bean instanceof StorageConfig) { String path = ((StorageConfig)bean).getPath(); File file = new File(path); if (!file.exists()) { file.mkdirs(); } } return bean; } /** * 即使什么都不做,也必须返回原来的Bean实例 */ @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; } }Bean配置
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd"> <!-- 注册Bean后处理器 --> <bean class="com.codeproject.jackie.springrecipesnote.springadvancedioc.PathCheckingBeanPostProcessor" /> <bean id="aaa" class="com.codeproject.jackie.springrecipesnote.springadvancedioc.Battery"> <property name="name" value="AAA" /> <property name="price" value="2.5" /> </bean> <bean id="cdrw" class="com.codeproject.jackie.springrecipesnote.springadvancedioc.Battery"> <property name="name" value="CD-RW" /> <property name="price" value="1.5" /> </bean> <bean id="dvdrw" class="com.codeproject.jackie.springrecipesnote.springadvancedioc.Battery"> <property name="name" value="DVD-RW" /> <property name="price" value="3.0" /> </bean> <bean id="shoppingCart" class="com.codeproject.jackie.springrecipesnote.springadvancedioc.ShoppingCart" scope="prototype" /> <bean id="cashier1" class="com.codeproject.jackie.springrecipesnote.springadvancedioc.Cashier" init-method="openFile" destroy-method="closeFile" > <property name="path" value="c:/cashier1" /> </bean> </beans>测试类
package com.codeproject.jackie.springrecipesnote.springadvancedioc; import java.io.IOException; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * @author jackie * */ public class BeanPostProcessorTest { @Test public void testBeanPostProcessor() throws IOException { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); Product aaa = (Product) applicationContext.getBean("aaa"); Product cdrw = (Product) applicationContext.getBean("cdrw"); Product dvdrw = (Product) applicationContext.getBean("dvdrw"); ShoppingCart shoppingCart = (ShoppingCart) applicationContext.getBean("shoppingCart"); shoppingCart.addItem(aaa); shoppingCart.addItem(cdrw); shoppingCart.addItem(dvdrw); Cashier cashier = (Cashier) applicationContext.getBean("cashier1"); cashier.checkout(shoppingCart); } }
注:书上说如果使用JSR-250注解@PostConstruct和@PreDestroy,并且一个CommonAnnotationBeanPostProcessor实例调用初始化方法,那么PathCheckingBeanPostProcessor不能正常工作,因为它的默认优先级低于CommonAnnotationBeanPostProcessor。经我实践证明,PathCheckingBeanPostProcessor仍然可以正常工作。
注:这里我们只关心TCP套接字,所以文章中说sock结构或者socket结构的时候都只针对TCP协议。
在测试内核模块时,内核会因为内存耗尽而panic,使用crash工具查看core文件,提示的信息是"Kernel panic - not syncing: Out of memory and no killable processes..."。也就是说内核没有内存可以使用,并且也没有进程可以杀死来释放内存。不用说,肯定是发生了内存泄露!写这篇文章是为了总结一下查找这个BUG的过程,反思一下。在这个过程中用到了很多工具和方法,写下来对遇到类似问题的人或许有些帮助。
内核中运行的程序主要是我的内核模块和应用层的HTTP服务器。所以造成内核泄露要么是内核模块,要么是HTTP服务器。
问题出在HTTP服务器的可能性不大,首先这个服务器已经上线了挺长时间,相对比较稳定;其次如果是应用进程占用的内存过多,内核完全会杀死这个用户进程来释放内存,也就不会出现“Out of memory and no killable processes”这样的信息。所以基本可以肯定问题出在内核模块上。为了保险,还是要看一下应用层的服务器进程占用了多少内存。其中一台服务器的内存比较小,所以在这个机器上问题很快就可以重现。在内存消耗量比较大(通过free -m可以看到)时,通过top命令查看应用服务器进程占用的RSS内存(也可以通过/proc/[pid]/statem文件查看),应用服务器占用的内存只有100多M。接着查看进程占用的文件数,也就是连接数,这个信息可以通过/proc/[pid]/fd目录获取。应用服务器占用的连接数也非常少。通过查看应用服务器实际占用的内存数和连接数,现在就要开始去内核模块中查找问题了。
我的内核模块在sysfs中注册了相应的项,通过这些可以看到当前占用的内存数量。这些项的统计信息都是在分配内存成功、释放之后更新的,可以实时反映当前的内存占用信息。为了方便,我编写了一个脚本,每隔一秒就把这些信息刷到终端上,如下图所示:
total_mem项的单位是KB,所以这里看到占用的内存大约为3M。内存统计的地方都已经反复地检查过了,所以这个量是可信的。这个结果不是我想看到的,如果统计出来的量大的话,我就会从内核模块分配和释放的地方去找问题。但是现在这个量这么小,很明显不是内核模块中调用kmalloc()或kmem_cache_alloc()(内核模块调用)分配的内存没有被释放。这个时候很容易陷入迷茫,估计很多人都会去反复地检查自己的代码,看看是否有内存没有释放的地方。如果我最开始也这样做的话,或许也能早点发现BUG,但是也有可能要花更长的时间。一般情况下,代码中有很多不同功能的模块,每个模块的功能复杂度不同。你去检查代码往往会忽略功能简单的模块,即使是在遇到BUG的时候。而且这样盲目地去遍历各个可能出问题的地方,也很不明智,因为你现在了解到的信息还太少。所以,这个时候,首先要做的不是去检查代码,而是首先要先通过各种工具和手段,来看一看内存究竟用到了什么地方。你现在遇到的问题,很多前辈们也遇到过。为了方便快捷地解决遇到的问题,前辈们已经为我们开发出了很多很多方便好用的工具,为什么不拿来用呢?貌似扯的有点远了,继续我们的问题.......
很庆幸看到了霸爷的《Linux Used内存到底哪里去了?》这篇文章,所在我决定先按照这篇文章的方法,找到系统被使用的内存都在哪里。正如文章中所说的,内存主要用3个去向:进程消耗、slab消耗和pagetable消耗。其中pagetable消耗是内核管理页面时的消耗,也就是struct page等结构的消耗。slab消耗不仅包括管理slab结构本身的消耗,还包括每个slab缓存的内存。slab缓存的内存在这三类中占用的量也是很大的。我们知道内核模块或驱动、内核活动本身分配内存,都是基于slab的。kmalloc()分配的内存也是从slab中获取的。按照霸爷的方法,我计算除了进程占用的内存、slab消耗的内存和pagetable占用的内存,这个值和free命令看到的基本吻合。这两个值会有偏差,因为会对共享库占用的内存重复计算。只要差别不大,就可以。我为什么要按照文章中的方法自己做了一次呢?很简单,首先我可以熟悉一些工具的使用,了解这些工具可以提供给我什么信息;其次,在这个过程中我可以知道怎么从/proc目录下找到一些内存相关的项,通过这些项可以看到系统的运行情况。
OK,现在计算了系统使用的内存量,并且和free看到的一样。但是这个时候我急于解决问题,心浮气躁,犯了一个很大的错误。我已经计算了进程消耗的内存量、slab消耗的内存量、pagetable消耗的内存量,这时我至少应该看一看这三个值哪个比较大吧?看看内存究竟在哪个地方消耗的多吧,可是我竟然没有!深深地鄙视一下自己!如果这时我只要留心看一下,我就会发现slab占用的内存非常大(后话,情况确实如此),这可以大大缩短我消耗的时间。如果发现slab占用的内存太大,可以用slabtop这个工具来看看哪个slab占用的内存过多(不同的slab有不同的名称)。slabtop显示的信息是根据/proc/slabinfo这个文件生成的。很幸运,虽然我犯了一个错误,可我还是使用了slabtop这个工具看查看slab消耗的内存情况(现在也不知道当时为什么要这样做)。当然这个时候我看这个工具的输出完全没有目的性,胡乱的看,第一次用总是很茫然。在看的过程中,我注意到了一个叫”TCP“的Slab的缓存大小一直在增加,而且增加的速度也挺快的。这时我意识到问题可能出现在这个Slab上,果断重启机器重新构造环境,并且单独打开了一个终端,在这个终端运行slabtop,这样在内核panic之后,你在终端(Xshell)仍然可以看到最近时间的slabtop输出。程序启动之后,我就开始在内核源码中找”TCP“对应哪个slab。首先从名称来看,这个肯定和TCP协议相关。看过内核协议栈的肯定都知道,当分配套接字的时候,会从tcp_prot变量的slab成员中分配sock结构,我首先找这个slab的名称是什么。tcp_prot变量的slab成员在定义的时候是没有初始化的,它是在注册到系统的时候才初始化,注册是在inet_init()函数中进行的,如下所示:
static int __init inet_init(void) { ...... rc = proto_register(&tcp_prot, 1); if (rc) goto out; ...... }而slab的创建是在prot_register()函数中,如下所示:
int proto_register(struct proto *prot, int alloc_slab) { if (alloc_slab) { prot->slab = kmem_cache_create(prot->name, prot->obj_size, 0, SLAB_HWCACHE_ALIGN | prot->slab_flags, NULL); if (prot->slab == NULL) { printk(KERN_CRIT "%s: Can't create sock SLAB cache!\n", prot->name); goto out; } ...... } ...... }结合上面两部分代码,可以看到这里创建的slab的名称是tcp_prot的name成员,那这个name成员是什么呢?看看tcp_prot的定义就知道了,如下所示:
struct proto tcp_prot = { .name = "TCP", .owner = THIS_MODULE, ...... };没错,就是看到的那个"TCP"slab就是tcp_prot的slab成员对应的那个。tcp_prot的slab是在inet_create()中创建sock结构的时候用到。此时"TCP"slab占用的内存过多,是因为有太多的sock结构没有释放导致的。
我的内核模块是用来做TCP连接迁移,会在两个地方创建sock结构,一个是在内核中创建新的连接时(和传统的方式不一样),一个是在发送迁移信息的时候(正常的TCP连接)。因为构建的连接在成功之后放到系统中了,后期的释放完全按照TCP协议的正常关闭流程,不由内核模块控制了,如果是这造成的,不好确定。所以先确定是否是因为发送迁移信息时创建的连接没有关闭导致的。好好检查了一番代码,基本排除了这种可能。下面就是我也确定是否是因为我创建的连接没有释放导致的,最先想到的就是在sk_free()处添加钩子(也就是kprobe机制)。当然,有Systemtap这个强大的脚本工具,自然不会再编写C代码,然后用insmod或modprobe插入模块的方式了。下面是我的sk_free.stp脚本:
%{ #include <linux/tcp.h> #include <net/tcp.h> #include <linux/sched.h> %} function sk_free_test:long(arg1:long) %{ struct sock *sk = (struct sock *)THIS->arg1; if (inet_sk(sk)->sport == htons(80)) { _stp_printf("func:sk_free_test, sk=%p, sk->sk_state=%u, sk->sk_wmem_alloc=%d,slab:%p,daddr=%d.%d.%d.%d\n", sk, sk->sk_state, atomic_read(&sk->sk_wmem_alloc), sk->sk_prot_creator->sl
C++里面有使用异常处理的机制,但是那些需要throw catch捕捉等操作
而通常一个win程序,我们会因为各种原因导致其crash,而不一定是throw 了某个exception。我们需要这样一个机制,可以在程序因为各种原因crash时掌握程序的情况,并能够做点事情,这就是所谓的“优雅的结束”。win api恰为我们提供了这样一个函数SetUnhandledExceptionFilter。
win程序在release情况下,发生了异常,并且没有被捕捉处理,就会调用一个默认的UnhandledExceptionFilter函数,这个函数还传入一个参数_EXCEPTION_POINTERS*,里面有crash的一些信息。
如果我们想介入这个挂掉的过程,比如优雅的结束,比如收集crash信息并上传,然后告诉用户你挂了。。那就可以自定一个函数如
long __stdcall MyUnhandledExceptionFilter (_EXCEPTION_POINTERS* ExceptionInfo){。。。
}
然后使用SetUnhandledExceptionFilter set进来。
MyUnhandledExceptionFilter 函数有三种返回值:
EXCEPTION_EXECUTE_HANDLER equ 1 表示我已经处理了异常,可以优雅地结束了
EXCEPTION_CONTINUE_SEARCH equ 0 表示我不处理,其他人来吧,于是windows调用默认的处理程序显示一个错误框,并结束
EXCEPTION_CONTINUE_EXECUTION equ -1 表示错误已经被修复,请从异常发生处继续执行
注意这些只在release下work