说起aop,大家都很熟悉。
①.Aop是什么呢?
通常来讲就是Aspect Oriented Programming的缩写,翻译成中文就是面向切面编程。
②.Aop能做什么呢?
我觉得,AOP能做的东西很多,最重要的就是在不动老结构的情况下增强原有的内容,或者是批量的对不同的功能进行统一加强。
当然啦,这不在今天的问题范围内。
在这里,我们只探讨如何使用javassist,完成简单的基于注解的AOP与依赖注入。
GOAL:
1.通过@Component 注解结合包路径扫描装载bean,并暂只支持一种单例生命周期。
3.通过@AopBefore 、@AopAfter 两个注解 标注前置增强与后置增强。
2.通过@Autowired 注解(仅字段)声明根据名称的自动装配。
为了让我们项目里的可爱的对象们都找到喜欢的小伙伴,所以我们非常有必要创建一个婚姻介绍所 (Context 类)!而本猿则化身正义的婚姻介绍所所长!代表月亮撮合你们!哼哼哼~
第一步,收集资料!
嗯哼,让我们来收集第一大街的单身狗资料吧。
Context类:
private final HashMap<String, ClassInfo> classMap = new HashMap<>();
private final HashMap<String, Object> singletonMap = new HashMap<>();
首先我向伟大的jvm申请领取两个免费的档案夹,一个classMap用于存放单身狗的投递资料。另外一个singletonMap用来存放单例生命周期的单身狗的真身。
然后,我到黄勇大哥的商店偷了一把全自动高科技地毯式对象资料搜索神器(没错,就是这里)。有此神器,本猿定能所向睥睨,把所有对象的底摸得不要不要的。
就是这么无耻的把包名都改了,哼哼哼。
Context类:
static ClassScanner scanner = new DefaultClassScanner();
任性的扔到我的婚介所里。
接下来,本猿要出动了!
public void loadFromPackage(String packageName) {
List<Class<?>> list = scanner.getClassListByAnnotation(packageName, Component.class);
for (Class<?> c : list) {
putBeansInfo(c);
}
}
哼哼,不是所有的对象,本猿都接受的哦,脑门上没贴个死字儿,阎王爷也不会来找你,哦,说错了,脑门上没有@Component的,本猿懒得鸟你们。
Component注解:
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Component {
String value() default "";
ContextType contextType() default ContextType.Singleton;
}
生命周期类型:
public enum ContextType {
Singleton;
}
Context类里面添加一下,先简单打印一下:
private void putBeansInfo(Class<?> c) {
System.out.println(c.getName());
}
试试:
建立三个类:Wangnima.java Doubi.java Laowang.java 其中Doubi 和Laowang 加@Component,Wangnima不加,
简单粗暴的main函数测试:
public static void main(String[] args) {
Context context=new Context();
context.loadFromPackage("com.weimo.learn.aop.testobjects");
}
结果:
接着干活:
private void putBeansInfo(Class<?> c) {//把类的信息存入两个map中
Component component = c.getAnnotation(Component.class);
String name = component.value();//获取bean名
ContextType type = component.contextType();
if (name.equals("")) {//默认值则把类名(不含包名)的第一个字母小写
name = c.getSimpleName();
Character fc = name.charAt(0);
fc = (fc >= 'A' && fc <= 'Z') ? (char) (fc + 32) : fc;
name = fc.toString() + name.substring(1);
}
ClassInfo ci = new ClassInfo();
ci.setTheClass(c);
ci.setType(type);
classMap.put(name, ci);//存入类信息
if (type == ContextType.Singleton) { //如果是声明单例,放入单例map中
Object bean;
try {
bean = c.newInstance();//实例化
singletonMap.put(name, bean);
} catch (InstantiationException | IllegalAccessException ex) {
Logger.getLogger(Context.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
添加getBean方法。
public Object getBean(String name) {
ClassInfo ci = classMap.get(name);
ContextType type = ci.getType();
if (type == ContextType.Singleton) {
return singletonMap.get(name);
}
return null;
}
简单的测试代码:
Context context=new Context();
context.loadFromPackage("com.weimo.learn.aop.testobjects");
Doubi doubi=(Doubi) context.getBean("doubi");
System.out.println(doubi);
结果:
这样就实现了我们的第一个目标啦,简单吧?
第二步,头发的特技!
这些单身狗对象们,可不是那么好伺候的,个个都是大爷,要求本猿的婚介所给打扮打扮,不然就要求退所!
面对这样无理的要求!!
我tmd竟然欣然接受了。
但是这群屌丝都已经成形了,咋改啊,要是一个一个改他们DNA(源代码),我还不得疯啊,而且这些屌丝一天一个想法, 万一哪天不想玩特技了咋整啊。
哼哼,这得用上俺的JAVA神功之动态代理大法。
动态代理就是动态的在运行时为目标类型创建代理类型。在这里我们将屌丝们声明的特技通过这种动态代理方式植入屌丝约会技能之中,从而在屌丝约会技能施法之前或之后执行。
我们的目标是:
将这样的类
通过@AopBefore 加上这样的特技
最后动态的创建Doubi的子类代理类,让在原方法调用之前先执行@AopBefore 指定类的特定方法。
最后让父类的变量指向代理类的对象。
好了,在Context里面创建getAopBean方法。
使用javassist,创建传入类的子类。
遍历传入类的所有声明方法,检查是否含有我们期望的含AopAfter或AopBefore的注解,将含有这两种注解之一的方法们,在新创建类型中重写,覆盖父类的方法。
private Object getAopBean(Class c) {
Random random = new Random();
String className = c.getName();
try {
CtClass ctClass = cp.get(className);
CtClass newClass = cp.makeClass(className + "_child" + random.nextInt(10));//创建新类型,起个名字
newClass.setSuperclass(ctClass);设置新创建类型的父类为传入的类
CtMethod[] ctMethods = ctClass.getDeclaredMethods();//获取所有父类声明的方法
for (CtMethod ctMethod : ctMethods) {
if (ctMethod.hasAnnotation(AopAfter.class) || ctMethod.hasAnnotation(AopBefore.class)) {
newMethod(ctMethod, newClass);//为新创建的类新建方法,代理父类对象的方法
}
}
newClass.writeFile();//保存
return newClass.toClass().newInstance();//
} catch (NotFoundException | CannotCompileException | ClassNotFoundException | IOException | InstantiationException | IllegalAccessException ex) {
Logger.getLogger(Context.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}
将putBeansInfo中,存放单例生命周期的bean从newInstance创建改为:bean = getAopBean(c);
这里是重写方法的实现:
private void newMethod(CtMethod ctMethod, CtClass newClass) throws NotFoundException, ClassNotFoundException, CannotCompileException {
StringBuilder codeBody = new StringBuilder("{");
String returnTypeName = ctMethod.getReturnType().getName();
CtClass[] parameterTypes = ctMethod.getParameterTypes();
boolean returnTypeNotVoid = !returnTypeName.equals("void");
AopBefore before = (AopBefore) ctMethod.getAnnotation(AopBefore.class);
if (before != null) {
Class<? extends AopInterceptor>[] value = before.value();
for (Class<? extends AopInterceptor> beforeDoClass : value) {
writeBeforeCode(beforeDoClass, codeBody, returnTypeNotVoid, returnTypeName);
}
}
writeTrueDoCode(ctMethod, codeBody, returnTypeNotVoid, parameterTypes);
AopAfter after = (AopAfter) ctMethod.getAnnotation(AopAfter.class);
if (after != null) {
Class<? extends AopInterceptor>[] afterInterceotors = after.value();
for (Class<? extends AopInterceptor> afterDoClass : afterInterceotors) {
writeAfterCode(ctMethod, afterDoClass, codeBody, returnTypeNotVoid, returnTypeName);
}
}
codeBody.append("}");
CtMethod newMethod = CtNewMethod.make(ctMethod.getReturnType(), ctMethod.getName(), parameterTypes, ctMethod.getExceptionTypes(), codeBody.toString(), newClass);
newClass.addMethod(newMethod);
codeBody = null;
}
下面是三部分代码编写的字符串拼接方法:
//关于前置的代码片段
private void writeBeforeCode(Class beforeDoClass,StringBuilder codeBody,boolean returnTypeNotVoid,String returnTypeName){
String beforeDoClassName = beforeDoClass.getName();
String beforeDoObjectName = beforeDoClass.getSimpleName().toLowerCase();
codeBody.append(beforeDoClassName).append(" ").append(beforeDoObjectName).append("= new ").append(beforeDoClassName).append("();");
codeBody.append(beforeDoObjectName).append(".invoke($args);");
codeBody.append("if(").
append(beforeDoObjectName)
.append(".isReturned()){");
if (returnTypeNotVoid) {
codeBody.append("return (").append(returnTypeName).append(")")
.append(beforeDoObjectName).append(".getReturnObject();");
} else {
codeBody.append("return;");
}
codeBody.append("}");
}
//调用父类方法执行实际要执行的内容
private void writeTrueDoCode(CtMethod ctMethod,StringBuilder codeBody,boolean returnTypeNotVoid,CtClass[] parameterTypes) throws NotFoundException{
if (returnTypeNotVoid) {
codeBody.append(ctMethod.getReturnType().getName())
.append(" result= ");
}
codeBody.append("super.")
.append(ctMethod.getName())
.append("(");
int len = parameterTypes.length;
for (int i = 0; i < len; i++) {
codeBody.append("$").append(i + 1);
if (i != len - 1) {
codeBody.append(",");
}
}
codeBody.append(");");
}
//关于后置的代码片段
private void writeAfterCode(CtMethod ctMethod,Class afterDoClass,StringBuilder codeBody,boolean returnTypeNotVoid,String returnTypeName){
String afterDoClassName = afterDoClass.getName();
String afterDoObjectName = afterDoClass.getSimpleName().toLowerCase();
codeBody.append(afterDoClassName).append(" ").append(afterDoObjectName).append("=").append("new ").append(afterDoClassName).append("();");
if (returnTypeNotVoid) {
codeBody.append(afterDoObjectName).append(".setReturnObject(result);");
}
codeBody.append(afterDoObjectName).append(".setMethodName(\"").append(ctMethod.getName()).append("\");");
codeBody.append(afterDoObjectName).append(".invoke($args);");
if (returnTypeNotVoid) {
codeBody.append("return result;");
}
}
好了,写完了,我们来测试一下吧!
public static void main(String[] args) {
Context context=new Context();
context.loadFromPackage("com.weimo.learn.aop.testobjects");
Doubi doubi=(Doubi) context.getBean("doubi");
doubi.BeMyDate("美女主播");
}
结果:
第三步,相亲大会!
next……