仿写Spring IOC和AOP,仿写思路和代码讲解

前言

因为自己在反射和代理这方面用的不多,所以掌握的不太好,正好在看spring的ioc和aop,这两个机制多多少少都用到了反射,尤其是aop,还用到了代理模式,所以借此机会简单仿写一下spring的ioc和aop,花了两天时间终于做好了,思路我觉得还是比较清晰的,记录在这里吧

源码已上传到GitHub上:Imitate-Spring-IOC-and-AOP

IOC仿写

说明一下,我只仿写了通过配置文件注入的方式,注解的方式等以后我可以也会做出来
如果不太清楚IOC的朋友可以看我写的另外一篇博客:IOC讲解
首先理清一下思路:

  • 肯定要有个实体类,通过配置文件的bean标签和property标签来完成注入
  • 扫描配置文件,找到所有的bean标签,获取到id和class,通过反射实例化bean对象
  • 扫描bean标签下的property,获取到name和value(也可能是ref),也是通过反射的方式把属性注入进去(这里可以用Field来直接注入,也可以用Method来调用set方法注入,还可以通过Constructor来调用构造器完成注入,我做的时候属性的直接注入和set注入都做了,构造器注入的原理也差不多)
  • 注入完成后以id作为key,bean对象作为value注册到一个HashMap中,这个HashMap就相当于IOC中的IOC容器,是单例模式的实现
  • 如果要获取到bean对象的话就是从HashMap中get到bean对象,相当于IOC中的getBean方法

以上就是大体的思路,下面就可以根据思路仿写了

实体类User
就注入int变量、String变量、int数组、String数组和引用类型这几个比较常用的吧,另外一些比如float、char、List、Set就不写了,思路也差不多,记得把set、get、toString加上

public class User {
    private String name;
    private int age;
    private int[] scores;
    private String[] subjects;
    private Book book;

    public User() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int[] getScores() {
        return scores;
    }

    public void setScores(int[] scores) {
        this.scores = scores;
    }

    public String[] getSubjects() {
        return subjects;
    }

    public void setSubjects(String[] subjects) {
        this.subjects = subjects;
    }

    public Book getBook() {
        return book;
    }

    public void setBook(Book book) {
        this.book = book;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", scores=" + Arrays.toString(scores) +
                ", subjects=" + Arrays.toString(subjects) +
                ", book=" + book +
                '}';
    }
}

实体类Book

public class Book {
    int number;

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    @Override
    public String toString() {
        return "Book{" +
                "number=" + number +
                '}';
    }
}

配置文件applicationContext.xml
保存了我们要注入的值

<beans>
    <bean id="java" class="MySpring.pojo.Book">
        <property name="number" value="22"/>
    </bean>

    <bean id="zzy" class="MySpring.pojo.User">
        <property name="name" value="zzy"/>
        <property name="age" value="20"/>
        <property name="scores">
            <array>
                <value>98</value>
                <value>66</value>
                <value>0</value>
            </array>
        </property>
        <property name="subjects">
            <array>
                <value>数学</value>
                <value>英语</value>
                <value>语文</value>
            </array>
        </property>
        <property name="book" ref="java"/>
    </bean>
</beans>

IOC核心类ScanXml
这个类完成了IOC的核心功能,包括扫描配置文件,生成bean对象,注入属性、注册Bean对象到容器中、getBean方法,注释掉的代码是通过Field直接注入的方式,没有注释掉的是通过Method来set注入的

整体思路:

  1. 使用HashMap作为IOC容器,注册register方法就是调用put方法,获取Bean对象getBean方法就是调用get方法,bean标签的id作为key,Bean对象作为value
  2. 扫描配置文件
  3. 得到bean的id和class
  4. 通过反射实例化Bean对象
  5. 得到property的name和value或者ref
  6. 利用反射得到这个属性和set方法,并获取它们的可访问权限
  7. 为了判断是不是数组属性,获取一下array标签,为空则不是,不为空另作处理
  8. 不是数组属性,如果是value,就直接通过invoke方法调用set方法来注入,如果是ref,先调用getBean方法得到容器中的Bean对象再invoke这个set方法注入
  9. 如果是数组,就先创建一个数组,再获得每个value标签,把每个value加到数组中,最后调用invoke方法调用set方法来注入
  10. 最后调用注册register方法把Bean对象注册到HashMap中
public class ScanXml {
    //作为bean容器
    private Map<String, Object> map = new HashMap<>();

	//实现IOC的核心方法,参数为配置文件路径
    public void Scanning(String classPath) throws Exception {
        //扫描配置文件
        InputStream inputStream = new FileInputStream(classPath);
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder docBuilder = factory.newDocumentBuilder();
        Document doc = docBuilder.parse(inputStream);
        Element root = doc.getDocumentElement();
        NodeList nodes = root.getChildNodes();

        //遍历<bean>
        for (int i = 0; i < nodes.getLength(); i++) {
            Node node = nodes.item(i);
            if (node instanceof Element) {
                Element element = (Element) node;
                //得到bean的id和class
                String id = element.getAttribute("id");
                String className = element.getAttribute("class");
                //根据class路径得到Class对象
                Class beanClass = null;
                try {
                    beanClass = Class.forName(className);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                    return;
                }
                //利用反射实例化bean对象
                Object bean = beanClass.newInstance();
                //得到所有property标签
                NodeList propertyList = element.getElementsByTagName("property");
                //遍历property标签
                for (int j = 0; j < propertyList.getLength(); j++) {
                    Node property = propertyList.item(j);
                    if (property instanceof Element) {
                        //得到property中的name,用来表示是哪个属性
                        String name = ((Element) property).getAttribute("name");
                        //可能是数组注入,所以要get一下array标签
                        Node arrayNode = ((Element) property).getElementsByTagName("array").item(0);
                        //利用反射得到这个属性和可访问权限
                        Field field = bean.getClass().getDeclaredField(name);
                        field.setAccessible(true);
                        //利用反射得到set方法和可访问权限,这里要拼一下字符串来指定你想要的这个set方法
                        Method method = bean.getClass().getDeclaredMethod("set" + (char) (name.charAt(0) - 32) + name.substring(1), field.getType());
                        method.setAccessible(true);
                        //判断是不是数组注入
                        if (arrayNode == null) {
                            //得到每个property中的value和ref,value是属性的值,ref是引用类型的值
                            String value = ((Element) property).getAttribute("value");
                            String ref = ((Element) property).getAttribute("ref");
                            //判断是value还是ref,哪个不为空就是哪个
                            //如果是value
                            if (!value.equals("") && ref.equals("")) {
//                                //判断value的数据类型
//                                if (field.getType().getName().equals("java.lang.String")) {
//                                    field.set(bean, value);
//                                } else if (field.getType().getName().equals("int")) {
//                                    field.set(bean, Integer.parseInt(value));
//                                }
								//判断是String类型还是int类型,这样做的目的是如果是int就需要让value转成int类型
                                if (field.getType().getName().equals("java.lang.String")) {
                                	//如果是String类型的属性,直接invoke这个set方法把value作为参数注入进去
                                    method.invoke(bean, value);
                                } else if (field.getType().getName().equals("int")) {
                                //如果是int类型的属性,先让value转成int类型,再invoke这个set方法注入进去
                                    method.invoke(bean, Integer.parseInt(value));
                                }
                                //如果是ref
                            } else if (!ref.equals("") && value.equals("")) {
                            	//调用一下getBean方法拿到这个引用类型的bean对象,再invoke这个set方法注入
                                method.invoke(bean, getBeanFormMap(ref));
//                                field.set(bean, getBean(ref));
                            } else {
                                throw new IllegalArgumentException("id为" + id + "的bean配置有误");
                            }
                            //如果是数组类型的属性
                        } else {
                            //标识是int数组还是String数组
                            boolean flag = false;
                            //得到多个value标签
                            NodeList valueList = ((Element) arrayNode).getElementsByTagName("value");
                            //根据数组类型创建数组
                            Object arr = Array.newInstance(field.getType().getComponentType(), valueList.getLength());
                            //如果是int数组,把标识改为true
                            if (field.getType().getComponentType().getName().equals("int")) {
                                flag = true;
                            }
                            //遍历每个value节点
                            for (int k = 0; k < valueList.getLength(); k++) {
                                Node valueNode = valueList.item(k);
                                //获取value
                                String value = valueNode.getTextContent();
                                if (value.equals("")) throw new IllegalArgumentException("id为" + id + "的bean配置有误");
                                //把value加到数组中,这里也需要考虑int和String两种情况
                                if (flag) {
                                    Array.set(arr, k, Integer.parseInt(value));
                                } else {
                                    Array.set(arr, k, value);
                                }
                            }
//                            field.set(bean, arr);
							//最后也是invoke这个set方法注入
                            method.invoke(bean, arr);
                        }
                    }
                }
                //前面的工作做完之后,已经得到了一个注入完成的bean对象了,最后将bean对象注册到bean容器中
                register(id, bean);
            }
        }
    }

    //将bean对象注册到bean容器中
    private void register(String id, Object bean) {
        map.put(id, bean);
    }

    //获取bean对象
    public Object getBeanFormMap(String name) throws Exception {
        Object bean = map.get(name);
        if (bean == null) throw new IllegalArgumentException("Bean容器中没有name为" + name + "的bean对象");
        return bean;
    }
}

ClassPathXmlApplicationContext类
这个类的作用是把获取配置文件路径到调用ScanXml类中的Scanning方法这一系列过程封装起来,并提供一个getBean方法来调用ScanXml类中的getBean方法获取到Bean对象,这样我们就可以直接调用ClassPathXmlApplicationContext类中的getBean方法就可以得到Bean对象了,不需要再先获取到配置文件路径再调用Scanning方法最后调用getBean方法了

public class ClassPathXmlApplicationContext extends ScanXml {
    private String classpath;

    public ClassPathXmlApplicationContext(String classpath) {
        this.classpath = Objects.requireNonNull(ScanXml.class.getClassLoader().getResource(classpath)).getFile();
    }

    public Object getBean(String name) throws Exception {
    	//调用ScanXml中的Scanning方法
        Scanning(classpath);
        //调用ScanXml的getBean方法
        return super.getBeanFormMap(name);
    }
}

测试类MyTest
直接调用ClassPathXmlApplicationContext 中的getBean方法就可以得到Bean对象了

public class MyTest {
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("MySpring/Resource/applicationContext.xml");
        User user = (User) context.getBean("zzy");
        System.out.println(user.toString());
    }
}

测试结果在这里插入图片描述

AOP仿写

是基于上面的IOC来仿写的
实现目标: 通过注解的方法来对IOC生成的Bean对象的方法前后进行功能增强
如果不太清楚AOP的朋友可以看我写的另外一篇博客:AOP讲解
也是先理清一下大致思路:

  • 通过@Aspect、@Pointcut、@Before、@After来自定义切面,@Aspect是用来标识这个类是一个切面类,@Pointcut是标识切入点的,其中的value标识了哪个类的哪个方法作为切入点,@Before和@After是用来标识这个方法是作用在哪里的,value可以是@Pointcut标识的切入点,也可以是直接上切入点,和Spring AOP的用法一样
  • 通过反射获取的自定义的切面类的所有信息,主要用到了getAnnotation方法获得注解的信息,包括切入点,哪个方法是通知方法,是用@Before修饰的还是用@After修饰的
  • 调用ScanXml类的Scanning方法和getBean方法来通过IOC得到这个Bean对象
  • 将上面得到的所有信息都传给通知类,通知类要实现InvocationHandler接口
  • 通过JDK动态代理来得到代理对象,在通知类的invoke方法中利用上面得到的所有信息来判断是否要利用反射调用通知方法以及何处调用

以上就是大体的思路,下面就可以根据思路仿写了

所有注解

//标识这个类是切面类
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Aspect {
    public String value() default "";
}
//记录切入点的注解,value用来记录
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Pointcut {
    String value() default "";
}
//标识这个方法是前置通知方法
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Before {
    String value();
}
//标识这个方法是后置通知方法
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface After {
    String value();
}

service层的接口和类
测试AOP用的

public interface UserService {
    void add(int i);
    
    void delete();
}
public class UserServiceImpl implements UserService{
    @Override
    public void add(int i) {
        System.out.println("添加了一条数据" + i);
    }

    @Override
    public void delete() {
        System.out.println("删除了一条数据");
    }
}

自定义切面AnnotationPointCut类
@Pointcut注解的value的字符串表示包名.类名.方法名,表示这个包下的这个类的哪个方法是切入点,*表示所有方法,也可以写为一个具体的方法名
@Before和@After表示这个方法是前置或后置通知方法,value可以是包名.类名.方法名,也可也是被@Pointcut修饰的方法名,表示@Pointcut中的value就是切入点

@Aspect
public class AnnotationPointCut {
    @Pointcut("MySpring.service.UserServiceImpl.*")
    public void point(){

    }

    @Before("point()")
    public void before(){
        System.out.println("方法执行前");
    }

    @After("point()")
    public void after(){
        System.out.println("方法执行后");
    }
}

ClassPathXmlApplicationContext类
和上面仿写的IOC用的是同一个,只不过重载了getBean方法,第二个getBean方法的多了一个Class参数,这个参数就是我们自定义的切面类,在这个getBean方法中要通过反射获取到注解的所有信息,最后把这些信息都传到通知类中,利用JDK动态代理生成代理对象,返回代理对象即可,所以大家只要看第二个getBean方法即可

public class ClassPathXmlApplicationContext extends ScanXml {
    private String classpath;

    public ClassPathXmlApplicationContext(String classpath) {
        this.classpath = Objects.requireNonNull(ScanXml.class.getClassLoader().getResource(classpath)).getFile();
    }

    public Object getBean(String name) throws Exception {
        Scanning(classpath);
        return super.getBeanFormMap(name);
    }
	
	//重载getBean方法,参数为自定义切面类
    public Object getBean(String name, Class cl) throws Exception {
    	//切入点的value
        String pointcutName = null;
        //用@Pointcut标识的方法
        Method pointcutMethod = null;
        //前置通知方法
        Method beforeMethod = null;
        //后置通知方法
        Method afterMethod = null;
        //前置通知切入点的value
        String beforeName = null;
        //后置通知切入点的value
        String afterName = null;
        //通过反射获取Aspect注解
        Aspect aspect = (Aspect) cl.getAnnotation(Aspect.class);
        //判断是不是切面类
        if (aspect == null) {
            throw new IllegalArgumentException("该类不是切面类");
        }
        //通过反射得到切面类的所有方法
        Method[] methods = cl.getDeclaredMethods();
        //遍历所有方法
        for (Method method : methods) {
        	//先获取Pointcut注解
            Pointcut pointcut = method.getAnnotation(Pointcut.class);
            if (pointcut != null) {
            	//得到Pointcut注解的方法
                pointcutMethod = method;
                //得到Pointcut注解的value
                pointcutName = pointcut.value();
                break;
            }
        }
        //再次遍历方法
        for (Method method : methods) {
            Boolean flag = null;
            String value = null;
            //获取Before和After注解
            Before before = method.getAnnotation(Before.class);
            After after = method.getAnnotation(After.class);
            if (before != null) {
                flag = true;
                value = before.value();
                //得到前置通知方法
                beforeMethod = method;
            }else if(after != null){
                flag = false;
                value = after.value();
                //得到后置通知方法
                afterMethod = method;
            }
            //如果Before的value是切入点方法,直接将前后置通知的value换成切入点的value
            if (flag != null && pointcutMethod != null && (value.equals(pointcutMethod.getName() + "()"))) {
                if(flag){
                    beforeName = pointcutName;
                }else{
                    afterName = pointcutName;
                }
            //如果Before的value不是切入点方法,直接获取前后置通知的value
            }else{
                if(flag != null){
                    if (flag) {
                        beforeName = value;
                    }else{
                        afterName = value;
                    }
                }
            }
        }
        //调用ScanXml的Scanning方法
        Scanning(classpath);
        //调用ScanXml的getBean方法获取Bean对象 
        Object bean = super.getBeanFormMap(name);
        //把上面得到的所有信息通过构造方法传给通知类Advice中,包括Bean对象,切面类对象,前后置通知方法,前后置通知的value
        Advice advice = new Advice(bean,cl.newInstance(),beforeMethod,beforeName,afterMethod,afterName);
        //jdk动态代理生成代理对象
        Object beanPorxy = AopProxy.getProxy(bean,advice);
        //返回代理对象
        return beanPorxy;
    }
}

通知Advice类

public class Advice implements InvocationHandler {
	//jdk动态代理要用到的bean对象,要在这个对象的方法前后使用aop做增强
    private Object bean;
    //自定义切面类的实例对象
    private Object invocationBean;
    //前置通知方法
    private Method beforeMethod;
    //前置通知value
    private String beforeName;
    //后置通知方法
    private Method afterMethod;
    //后置通知value
    private String afterName;
	
	//构造方法
    public Advice(Object bean, Object invocationBean, Method beforeMethod, String beforeName, Method afterMethod, String afterName) {
        this.bean = bean;
        this.invocationBean = invocationBean;
        this.beforeMethod = beforeMethod;
        this.beforeName = beforeName;
        this.afterMethod = afterMethod;
        this.afterName = afterName;
    }
	
	//jdk动态代理的invoke方法,用来实现aop
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    	//要使用aop的类的类名
        String invocationClass = bean.getClass().getSimpleName();
        //前置通知的作用类的名,从value中得到
        String beforeClass = null;
        //后置通知的作用类的名,从value中得到
        String afterClass = null;
        //前置通知的作用方法,从value中得到
        String beforeBeanMethod = null;
        //后置通知的作用方法,从value中得到
        String afterBeanMethod = null;
        //用来存储前置通知的value中的信息,后面要通过split方法分割
        String[] beforeArr;
        //用来存储后置通知的value中的信息,后面要通过split方法分割
        String[] afterArr;
        if(beforeName != null) {
        	//按照.分割字符串
            beforeArr = beforeName.split("\\.");
            for (String before : beforeArr) {
            	//如果某个字符串和使用aop的类名相等,就说明这个通知方法会作用在这个类上
                if(before.equals(invocationClass)){
                    beforeClass = before;
                }
            }
            //把使用aop增强的方法名获取到,位于value的最后面
            beforeBeanMethod = beforeArr[beforeArr.length - 1];
        }
        //和上面的逻辑一样
        if(afterName != null) {
            afterArr = afterName.split("\\.");
            for (String after : afterArr) {
                if(after.equals(invocationClass)){
                    afterClass = after;
                }
            }
            afterBeanMethod = afterArr[afterArr.length - 1];
        }
        //如果IOC获取的Bean的类和前置通知要增强的类一样,并且方法名也一致,就通过反射调用这个方法
        if(beforeMethod != null && beforeClass != null && (beforeBeanMethod != null && (beforeBeanMethod.equals("*") || beforeBeanMethod.equals(method.getName())))){
            beforeMethod.invoke(invocationBean);
        }
        //通过method调用IOC获取的类的方法
        method.invoke(bean,args);
                //如果IOC获取的Bean的类和后置通知要增强的类一样,并且方法名也一致,就通过反射调用这个方法
        if(afterMethod != null && afterClass != null && (afterBeanMethod != null && (afterBeanMethod.equals("*") || afterBeanMethod.equals(method.getName())))){
            afterMethod.invoke(invocationBean);
        }
        return null;
    }
}

测试类MyTest

public class MyTest {
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("MySpring/Resource/applicationContext.xml");
        //调用重载的那个getBean方法,后面那个参数是切面类
        UserService user = (UserService) context.getBean("userService",AnnotationPointCut.class);
        user.add(666);
        user.delete();
    }
}

测试结果
在这里插入图片描述
修改一下切面类
before方法是前置通知,只增强于UserServiceImpl类的add方法
after方法是后置通知,只增强于UserServiceImpl类的delete方法

@Aspect
public class AnnotationPointCut {
    @Before("com.zzy.service.UserServiceImpl.add")
    public void before(){
        System.out.println("方法执行前");
    }

    @After("com.zzy.service.UserServiceImpl.delete")
    public void after(){
        System.out.println("方法执行后");
    }
}

测试结果
在这里插入图片描述
至此,Spring IOC和AOP的仿写就结束了,本人能力有限,所以可能出现不足之处,欢迎指正
也希望大家可以在GitHub上拉下来自己看一下,思路会更加清楚

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值