spring自定义标签

前言

最近由于工作需要,需要在spring中自定义配置文件,解析、加载并使用自定义bean。看了一下相关的资料,这里做一个简单的总结。

准备

首先,你需要在maven工程resources/META-INF创建三个文件spring.schemas、spring-scf.xsd、spring.handlers。这三个文件是spring自定义标签的入口,具体内容和功能如下:

1、spring.schemas,用来指定xsd文件的位置,其地址和xsd文件名可以自定义,相当于是根入口

http\://www.chinahr.com/schema/scf.xsd=META-INF/spring-scf.xsd

2、spring-scf.xsd,用来规定自定义配置xml文件的格式,其中targetNamespace用来关联handlers控制器,xs:element的name属性用来指定自定义xml的标签名称

<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="http://www.chinahr.com/schema/scf" xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="service">
        <xs:complexType>
            <xs:simpleContent>
                <xs:extension base="xs:string">
                    <xs:attribute type="xs:string" name="id" use="required"/>
                    <xs:attribute type="xs:anySimpleType" name="interface" use="required"/>
                    <xs:attribute type="xs:string" name="url" use="required"/>
                    <xs:attribute type="xs:string" name="implClass" />
                </xs:extension>
            </xs:simpleContent>
        </xs:complexType>
    </xs:element>
</xs:schema>

3、spring.handlers,用来指定解析自定义xml的控制器,其实就是一个key-value键值对,注意key与xsd中的targetNamespace一致

http\://www.chinahr.com/schema/scf=com.bj58.scf.bean.ScfNamespaceHandlerSupport

自定义xml

在编写上面三个文件之后,我们就可以自定义如下格式的xml配置文件了

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:scf="http://www.chinahr.com/schema/scf"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
     http://www.chinahr.com/schema/scf http://www.chinahr.com/schema/scf.xsd">

    <scf:service id="courseService" interface="com.bj58.jyfz.train.contract.CourseService"
                 url="tcp://fortune/CourseServiceImpl"/>

    <scf:service id="trainUserService" interface="com.bj58.jyfz.train.contract.TrainUserService"
                 url="tcp://fortune/TrainUserServiceImpl"/>
</beans>

注意需要引入如下自定义的schema和namespace用于编写和校验xml,否则自定义xml会报错。仔细看下自定义xml节点的组成scf:service,这个"service"是在xsd中的<xs:element name="service">定义的

xmlns:scf="http://www.chinahr.com/schema/scf"
xsi:schemaLocation="http://www.chinahr.com/schema/scf http://www.chinahr.com/schema/scf.xsd"

控制

自定义xml编写完成之后需要解析,否则spring也会一脸懵逼的。回想一下上面的spring.handlers,里面定义了解析xml的控制器,其代码如下:

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

/**
 * @author zhangyining on 19/2/18 018.
 */
public class ScfNamespaceHandlerSupport extends NamespaceHandlerSupport {
    @Override
    public void init() {
        //解析并注册beanDefinition,service是xsd中的名字
        this.registerBeanDefinitionParser("service", new ScfBeanDefinitionParser());
//        this.registerBeanDefinitionParser("client", new ClientBeanDefinitionParser());
    }
}

通过继承抽象类NamespaceHandlerSupport,并重写其init()方法,这个init()方法指定了自定义xml的名称以及使用什么解析器去解析。换句话说,如果我们想同时解析两种不同的自定义xml,只需要在init()方法里调用两次registerBeanDefinitionParser,并指定对应的名称和解析器即可(有时候类的命名很重要,这个类我们之所以称之为控制器也是有道理的)

解析

终于到了解析这一步,上面的控制器中我们告诉spring使用自定义的ScfBeanDefinitionParser解析xml,那么看下其代码:

import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;

/**
 * @author zhangyining on 19/2/18 018.
 */
public class ScfBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {

    @Override
    protected Class<?> getBeanClass(Element element) {
        //此处返回的是自定义的FactoryBean,具体参见ScfFactoryBean
        return ScfFactoryBean.class;
    }

    /**
     * 重写父类空方法。主要工作是从element中获取自定义元素值,检查并设置到builder中
     */
    @Override
    protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {

        //get attribute
        String url = element.getAttribute("url");
        String iface = element.getAttribute("interface");
        String implClass = element.getAttribute("implClass");
        String id = element.getAttribute("id");

        //check attribute,id can not be duplicate
        if(StringUtils.hasText(id) && parserContext.getRegistry().containsBeanDefinition(id)) {
            throw new IllegalArgumentException(" interface has exist can't init another one id is " + id);
        } else {
            if(StringUtils.hasText(url)) {
                //set builder property
                builder.addPropertyValue("url", url);
            }

            Class<?> clazz;
            if(StringUtils.hasText(iface)) {
                try {
                    clazz = Class.forName(iface);
                    builder.addPropertyValue("interfaceClass", clazz);
                } catch (ClassNotFoundException e) {
                    throw new IllegalArgumentException("interface not found " + iface);
                }
            }

            if(StringUtils.hasText(implClass)) {
                try {
                    clazz = Class.forName(implClass);
                    builder.addPropertyValue("implClass", clazz);
                } catch (ClassNotFoundException var9) {
                    throw new IllegalArgumentException("implClass not found " + implClass);
                }
            }
        }
    }
}

doParse()方法不必多说,三个参数也比较简单,里面基本都是对参数的获取、校验并set到builder中。

拓展

需要说明一下上面getBeanClass方法,为什么重写这个方法以及ScfFactoryBean是什么。老样子,还是贴上代码:

import lombok.Data;
import org.springframework.beans.factory.FactoryBean;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * @author zhangyining on 18/12/19 019.
 */
@Data
public class ScfFactoryBean<T> implements FactoryBean {

    /**
     * 属性名称需要与ScfBeanDefinitionParser中放入builder中的一致
     * tcp访问url
     */
    private String url;

    /**
     * 接口
     */
    private Class<?> interfaceClass;

    /**
     * 实现类
     */
    private Class<?> implClass;

    private MapperInvocationHandler handler = new MapperInvocationHandler();

    /**
     * 属性建议通过set方法注入,构造方法注入容易出错
     */
    public ScfFactoryBean(){

    }

    @Override
    @SuppressWarnings("unchecked")
    public T getObject() throws Exception {
        return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{interfaceClass}, handler);
    }

    @Override
    public Class<?> getObjectType() {
        return interfaceClass;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

    private static class MapperInvocationHandler implements InvocationHandler{

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println(method.getName());
            return null;
        }
    }

这是一个FactoryBean,关于FactoryBean是什么,估计也是spring面试常问的题。通过其名字可以知道,FactoryBean也是一个bean,只不过有点特殊,获取这个bean时实际拿到的是getObject()返回的对象。通俗的讲,FactoryBean就是一层包装,实际的内容与getObject()返回值有关。这里相当于是一个延伸,因为很多时候,我们向spring加入的bean都是一个接口,然后通过代理模式去搞一下,这里只打印一下方法的名称。不过需要强调一点,mybatis-spring不是这么搞的,后续给大家补上mybatis-spring的实现方式。

还有一点需要强调一下,这个自定义FactoryBean中的属性名称和类型一定要与ScfBeanDefinitionParser中set到builder中保持一致,并且推荐使用set方法完成设置(只要为每个属性添加set方法,spring会自动设置。构造函数的方式试了几次没成功,不想具体深入研究了,意义不大。spring的依赖注入推荐使用set,构造函数如果出现循环依赖是无法完成注入的,有兴趣的可自行谷歌或百度)

测试

import com.bj58.jyfz.train.contract.CourseService;
import com.bj58.jyfz.train.contract.TrainUserService;import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.io.File;

/**
 * @author zhangyining on 19/2/18 018.
 */
public class Test {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        CourseService courseService = (CourseService) context.getBean("courseService");
        TrainUserService trainUserService = (TrainUserService) context.getBean("trainUserService");
        try {
            courseService.getCourseById(100002L);
            trainUserService.selectById(11L);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

测试方法能够正确的打印方法名称,测试结果如下:

getCourseById
selectById

总结

上面说了这么多其实只是一个架子,实际应用中,关于自定义的FactoryBean中如何执行想要的功能才是重点。spring对外提供了很多入口,方便用户自定义创建bean或做一些自定义的工作。但请紧急一点,对于自定义的bean,请务必从BeanDefinition入手!!!我们可以这样理解,spring先对所有的配置文件进行解析,转成BeanDefinition对象并存储到一个容器里,然后再通过BeanDefinition实例化bean完成注入。所以如果想创建自定义的bean,那么你只需要将自定义的BeanDefinition放入容器中即可,后续为大家简单说一下mybatis-spring是如何通过代码(不用自定义xml)完成mapper接口的注入的

附录

整体源码地址:https://github.com/yiniing/scf-spring.git

看过这个之后,再看mybatis-spring的实现已经很轻松了,所以不再单独写一篇随笔,直接附上git地址

mybatis-spring的简单实现:https://github.com/yiniing/simple-mybatis-spring.git

posted @ 2019-02-18 15:30  _Emotion丶小寳  阅读(380)  评论(0编辑  收藏  举报