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