struts2自定义数据类型转换器(Struts2)

斜浩穰
2023-12-01
  1. 写这篇文章的缘由,同学叫我帮忙搭建立ssi(Struts2+Spring+ibatis)框架,架构搭建成功后,写了个页面测试。其中有日期类型,所以就涉及到了Struts2的自定义数据类型转换器。(Strurts2有默认的日期转换器,但是支持的格式单一)
  2. 由于信息量比较大,文章的内容是关于"自定义数据类型转换器"一些底层的实现过程,会涉及到很多东西。所有难免有纰漏。
  3. 分享给希望了解“自定义数据类型转换器”的来龙去脉的同学,希望对你们有点帮助。纯手工写作,喜欢就点个赞。

-------------------------------概述------------------------------------------
从以下两点来认识Struts2中的自定义类型转换器,你能得到什么?你可以掌握它的整个生命周期。

一、自定义数据类型转换器初始化

二、使用自定义数据类型转换器(Struts2框架来完成),
只需要在xwork-conversion.properties配置自定义的数据类型转换器,
和实现自定义类型转换器(通过继承DefaultTypeConverter,也可以继承其子类StrutsTypeConverter)即可。

---------------------------------来龙去脉-------------------------------------------
 一、自定义数据类型转换器初始化,具体过程

1.服务器启动,加载和管理web.xml中的Filter:

<filter>
	<filter-name>struts2</filter-name>
		<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>

2.调用StrutsPrepareAndExecuteFilter类的初始化方法:init(FilterConfig) void
//初始化调度器Dispatcher(笔者叫法,感觉很贴切)

  Dispatcher dispatcher = init.initDispatcher(config);

3.调度器Dispatcher中进行加载和解析配置文件,初始化IOC容器等的初始化工作,主要初始化方法如下:

   Container container = init_PreloadConfiguration();
	
	接着,加载容器:reloadContainer(List<ContainerProvider>)

4.设置引导容器,比如 XWorkConverter(数据类型转换器,此文章关注点)
// Set the bootstrap container for the purposes of factory creation

   Container bootstrap = createBootstrapContainer();

5.DefaultConfiguration中使用构造器模式构建引导程序,源码片段如下:

  protected Container createBootstrapContainer() {
        ContainerBuilder builder = new ContainerBuilder();
        builder.factory(ObjectFactory.class, Scope.SINGLETON);
        builder.factory(ReflectionProvider.class, OgnlReflectionProvider.class, Scope.SINGLETON);
        builder.factory(ValueStackFactory.class, OgnlValueStackFactory.class, Scope.SINGLETON);
		//类型转换器
        builder.factory(XWorkConverter.class, Scope.SINGLETON);
        builder.factory(XWorkBasicConverter.class, Scope.SINGLETON);
        builder.factory(TextProvider.class, "system", DefaultTextProvider.class, Scope.SINGLETON);
        builder.factory(ObjectTypeDeterminer.class, DefaultObjectTypeDeterminer.class, Scope.SINGLETON);
        builder.factory(PropertyAccessor.class, CompoundRoot.class.getName(), CompoundRootAccessor.class, Scope.SINGLETON);
        builder.factory(OgnlUtil.class, Scope.SINGLETON);
        builder.constant("devMode", "false");
		//使用struts2 IOC容器创建引导程序
        return builder.create(true);
    }

6.Struts2 IOC容器通过Constructor反射(Struts2 IOC容器创建对象的其中一种方式)来调用XWorkConverter类的构造方法(使用struts2 IOC容器创建引导程序时)

  protected XWorkConverter() {
	   }

7.创建XWorkConverter实例后,进行依赖注入(@Inject注解 + IOC容器再配合一系列操作来完成)

 ContainerBuilder类中的方法: create(boolean) Container
	  public Container create(boolean loadSingletons) {
		ensureNotCreated();
		created = true;
		//初始化Struts2 IOC容器中的对象工厂factories
		final ContainerImpl container = new ContainerImpl(
			new HashMap<Key<?>, InternalFactory<?>>(factories));
		if (loadSingletons) {//单例
		  container.callInContext(new ContainerImpl.ContextualCallable<Void>() {
			public Void call(InternalContext context) {
			  for (InternalFactory<?> factory : singletonFactories) {
				//创建XWorkConverter等对象
				factory.create(context);
			  }
			  return null;
			}
		  });
		}
		//依赖注入
		container.injectStatics(staticInjections);
		return container;
	  }

8.依赖注入时,调用XWorkConvert类中的setObjectFactory方法,
此方法作用就是设置对象工厂和加载类路径下的xwork-conversion.properties和另一个properties文件。
xwork-conversion.properties这个配置文件中配置自定义的类型转换器,比如:java.util.Date=org.yl.util.DateConverterUtil
特别提醒:java.util.Date=org.yl.util.DateConverterUtil后面不要留有空格,否则报错误。
源码片段如下:

 @Inject
     public void setObjectFactory(ObjectFactory factory) {
        //设置对象工厂,这个东西干嘛的?创建对象的。
		//(Strut2中(1)基层对象创建由IOC容器来创建,比如创建ObjectFactory。
		//(2)业务层对象创建通常是ObjectFactory,
		//比如:Action, interceptor,Result还有这里的自定义类型转换器等)
		this.objectFactory = factory;
        // note: this file is deprecated
        loadConversionProperties("xwork-default-conversion.properties");
		
		//加载类路径下的xwork-conversion.properties文件
        loadConversionProperties("xwork-conversion.properties");
    }

9.把配置文件中的自定义转换器放到Map容器中,key为"java.util.Date",value为:org.yl.util.DateConverterUtil
//比如笔者的xwork-conversion.properties中的配置:java.util.Date=org.yl.util.DateConverterUtil
//为什么把它放到Map容器中?
//存到Map中的目的是便于后面使用org.yl.util.DateConverterUtil时,get()提取出来使用。(是不是有点费话,我也觉得!哈哈。)
protected HashMap<String, TypeConverter> defaultMappings = new HashMap<String, TypeConverter>();

方法:loadConversionProperties(String, boolean) void 片段,如下:

 try {
			//创建自定义类型转换器,org.yl.util.DateConverterUtil
			TypeConverter _typeConverter = createTypeConverter((String) entry.getValue());
			if (LOG.isDebugEnabled()) {
				LOG.debug("\t" + key + ":" + entry.getValue() + " [treated as TypeConverter " + _typeConverter + "]");
			}
			//把org.yl.util.DateConverterUtil放入Map中
			//key为xwork-conversion.properties配置文件中的"java.util.Date"
			//value为xwork-conversion.properties配置文件中的org.yl.util.DateConverterUtil实例对象
			defaultMappings.put(key, _typeConverter);
		} catch (Exception e) {
			LOG.error("Conversion registration error", e);
		}

休息一小会,接下来看看,Struts2框架是如何使用开发人员自定义的数据类型转换器的。马上就OK了,继续往下看,有不错的收获喔!

二、使用自定义数据类型转换器
1。前端页面(假设含有表单数据)向服务器发送一个请求,访问服务器中的一个项目(比如叫:ylstruts2,这个项目MVC框架使用的是Struts2)

2。web.xml中配置了"映射配置",拦截所有的请求(包括静态页面)

<filter-mapping>
		<filter-name>struts2</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

3.当前端发送一个请求时,Struts2框架的StrutsPrepareAndExecuteFilter会调用以下方法:

doFilter(ServletRequest, ServletResponse, FilterChain) : void

此方法做了许多工作,这里大家只需关注与Struts2自定义数据类型转换器相关的工作。

 包装请求: request = prepare.wrapRequest(request); 通过request获取前端表单中的数据(ajax类型这里不讨论:struts2中有四种方式处理ajax请求)

4.执行Action,前面一些准备工作,如:Mapping,ActionProxy,获取Action的拦截器等不讨论
execute.executeAction(request, response, mapping);

4.1 执行Action前要经过Action的拦截器,工作在DefaultActionInvocation类的invoke()方法中进行,源码片段如下:
这个invoke方法其实使用的是一个责任链设计模式来实现AOP。

 if (interceptors.hasNext()) {
                final InterceptorMapping interceptor = (InterceptorMapping) interceptors.next();
                String interceptorMsg = "interceptor: " + interceptor.getName();
                UtilTimerStack.push(interceptorMsg);
                try {
                                resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this);
                            }
                finally {
                    UtilTimerStack.pop(interceptorMsg);
                }
            } else {
                resultCode = invokeActionOnly();
            }

4.2 其中一个重要的拦截器ParametersInterceptor,从ActionContext中获取前端请求的参数(前面第3项中包装request请求时,把前端参数放置到了ActionContext中)

   final Map<String, Object> parameters = retrieveParameters(ac);

4.3.接着ParametersInterceptor拦截器,把前端参数封装到后端Action中对应的属性中。这个过程,涉及Ognl,OgnlValueStack和XWorkConverter等。
这个封装过程可以说是由强大的Ognl来完成的,其中有数据类型转换(此文章所关注的),关于数据类型转换使用到的是Struts2框架中的转换器。
(题外话,Struts2框架对Ognl中的数据类型转换器,做了两件事:
1。原原本本的Copy了Ognl原来的数据类型转功能,然后给类改了一下名称。
2。对Ognl数据类型转换器进行了扩展,比如文本类型转换成Date,这个功能在原生的Ognl框架中是没有的。)

5.数据类型转换,Ognl + OgnlValueStack + XWorkConverter 组合进行。
ParametersInterceptor类的setParameters(Object, ValueStack, Map<String, Object>)方法进行了以下业务逻辑处理:
//5.1 把前端参数设置到Action中对应的属性中.它是怎么把前端数据设置到后端的Action属性中的?
//它又是如何知道前端的弱类型数据要转换成java后端强类型数据的那一种(是int,Long,Float,Date还是Object)
//我用四个字来解决这两个问题:“控制反转”,具体的思路分析不在此细述。

	for (Map.Entry<String, Object> entry : acceptableParameters.entrySet()) {
        String name = entry.getKey();
        Object value = entry.getValue();
        try {
			//OgnlValueStack设值
            newStack.setValue(name, value);
        } catch (RuntimeException e) {
        }
    }

//5.2 借助Ognl设值

  ognlUtil.setValue(expr, context, root, value);
	  Ognl.setValue(compile(name), context, root, value);

//5.3 Ognl框架中使用AST抽象语法树,得到ASTChain(如果使用的是user.name这种形式的表达式时)

//5.4 取得访问器和数据类型转换器,比如ObjectAccessor访问器,使用对象中对应的的setXXX方法,比如:setValidateDate。
在给java后端的Action属性赋值前,先进行类型转换。比如前端参数"1986/05/16",后端用java.util.Date validateDate来接收前端参数。
那么就得先给弱类型"1986/05/16"转换成java强类型数据Date,此时需要获取"数据类型转换器"
用到OgnlRuntime类中的getConvertedType(OgnlContext, Object, Member, String, Object, Class) 方法。

typeConverter.convertValue(context, target, member, propertyName, value, toType);
typeConverter为XWorkConverter,调用是的convertValue方法,进行以下逻辑处理:
//5.4.1 when converting from a string, use the toClass’s converter

tc = lookup(toClass);

//5.4.2 Looks for a TypeConverter in the default mappings.

 public TypeConverter lookup(Class clazz) {
			return lookup(clazz.getName());
		  }

//5.4.3 紧接着调用以下方法,这个熟悉吧,就是第一条初始化时的那个Map,从这个Map中找"java.util.Date"为key的数据类型转换器

//protected HashMap<String, TypeConverter> defaultMappings = new HashMap<String, TypeConverter>();
		  public TypeConverter lookup(String className) {
			if (unknownMappings.contains(className) && !defaultMappings.containsKey(className)) {
				return null;
			}
	    //取得HashMap中key为:"java.util.Date", value为:org.yl.util.DateConverterUtil实例对象
		//(这里有必要说明一下:取到的org.yl.util.DateConverterUtil实例对象与初始化时put进去的org.yl.util.DateConverterUtil实例对象
		//不是同一个实例对象(我怎么知道的?因为它俩的内存地址不一样),我推测从初始化到使用这个过程有一定的时间段,
		//不可能一直存在内存中,可能被GC回收了。所以借助ObjectFactory重新创建了DateConverterUtil实例对象,导致存与取两个操作时的。
		//org.yl.util.DateConverterUtil实例对象不是同一个对象。有兴趣的同学可以去验证下。)
		//这里的className为"java.util.Date"
		TypeConverter result = defaultMappings.get(className);
		…………
	}

//5.4.4 取得自定义类型转换器后,调用自定义数据类型转的器的convertValue方法,
//若没有获取到自定义的数据类型转换器,使用默认的数据类型转换器defaultTypeConverter,
//获取到了自定义数据类型转换器后,进行如下操作:

	tc.convertValue(context, target, member, property, value, toClass);

tc:为自定义数据类型转换器对象的实例:org.yl.util.DateConverterUtil@16bd01d。
笔者的自定义数据类型转换器org.yl.util.DateConverterUtil中的代码片段,如下:

	/* 支持转换的日期格式 */
	private static final DateFormat[] SUPPORT_DATE_FORMATS = {
						new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"),//这个要放在new SimpleDateFormat("yyyy-MM-dd")的前面,防止截掉HH:mm:ss。
						new SimpleDateFormat("yyyy-MM-dd"),
						new SimpleDateFormat("yyyy/MM/dd HH/mm/ss"),
						new SimpleDateFormat("yyyy/MM/dd"),
						};
						
	 public Object convertValue(Map<String,Object> context, Object value, Class toType) {
		if (toType == Date.class) { 
			//参数是数组类型,是使用了request.getParameterMap()后得到的数据key-value(数组类型),
			//使用ParameterInterceptor + ongl把value传递至此,进行数据类型转换的。
			String[] params = (String[]) value;
			//数组中元素是一个所以取params[0]
			String dateString = params[0];
			for (DateFormat format : SUPPORT_DATE_FORMATS) {
				try {
					//遍历支持的日期格式把文本类型转换java.util.Date
					return format.parse(dateString);
				} catch (ParseException e) {
					continue;
				}
			}
			return null;

		}

6.数据类型转换完成后,把转换后的数据设置到后端的Action对应的属性中(具体怎么设置的还得细说Ognl,不是本文章所关注的重点)。OK完成。

写了这么多,不知道对大家有没有点帮助。希望对大家有点帮助,觉得有用点个赞呗,纯手工写作。

 类似资料: