本篇目的:
梳理一下Spring+SpringMVC与SpringBoot:
预期效果:
生成一个springmvc-default-configuration.jar包,集成了Spring+SpringMVC无xml配置,在新的Spring+SpringMVC项目开始时,导入springmvc-default-configuration.jar,在项目一级目录下新建一个类,对其做出一些简单的配置后,即可完成一个普通Spring+SpringMVC项目的全部配置。
.原理:
servlet3.0开始,WebApplicationInitializer接口可取代web.xml,通过实现WebApplicationInitializer,在其中可以添加servlet,listener等,在加载Web项目的时候会加载这个接口实现类,从而起到web.xml相同的作用,详细的接口的说明可以参考这篇文章:Spring中WebApplicationInitializer的理解
想要做出上述的springmvc-default-configuration.jar包,分两步:
1.可用版本:先搞一个无xml的Spring+SpringMVC demo,先保证能用
2.封装版本:再 1 的基础上提取公共部分和可扩展部分进行封装
回顾SpringMVC在Web容器中的执行原理与配置过程,我们可以大致分为以下几步:
AbstractAnnotationConfigDispatcherServletInitializer实现了WebApplicationInitializer接口,现在我们只需要继承AbstractAnnotationConfigDispatcherServletInitializer抽象类实现以下三个方法即可代替以上步骤。
创建maven项目如下
ps:含有web.xml是为了可通过IDEA更方便的配置Tomcat,说白了就是告诉IDEA这是一个web项目,web.xml就是证明,但是web.xml什么都没写。
AbstractAnnotationConfigDispatcherServletInitializer抽象类实现
public class BaseSpringMvcDefaultConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* 相当于添加了Spring的容器
* @return
*/
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{RootConfig.class};
}
/**
* 相当于添加了SpringMVC的容器
* @return
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
/**
* 相当于配置了DispatcherServlet的 /
* @return
*/
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
RootConfig
很熟悉的注解,@Configuration:表明这是一个配置类,相当于一个spring的xml配置文件,对于RootConfig,我们让它扫描service和dao包
@ComponentScan(basePackages = {"mo.springmvc.defaultconfiguration.service",
"mo.springmvc.defaultconfiguration.dao"},
excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)})
@Configuration
public class RootConfig {
}
WebConfig
我们只让它扫描controller包
@ComponentScan(basePackages = {"mo.springmvc.defaultconfiguration.controller"},
includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)})
@EnableAspectJAutoProxy
@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
/**
* 配置静态资源路径
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
// registry.addResourceHandler("").addResourceLocations("");
super.addResourceHandlers(registry);
}
}
这三个类可称为“配置三剑客”
@Controller
public class HelloController {
@Autowired
HelloService service;
@RequestMapping("/test")
@ResponseBody
public String hello() {
return service.getHelloString();
}
}
@Service
public class HelloService {
public String getHelloString() {
return "Hello World!";
}
}
测试ok,可用版本出炉,但仔细想想,如果每个项目都这样配置,都要复制3个配置类,每次都要指定包扫描路径,还是过于重复了,所以还没达到我们预期的目标
很显然上述配置关键入口便是扫描包的路径,思考一下,程序启动时,第一步扫描WebApplicationInitializer实现类是系统自动完成的,那么如果我们能在第二步之前设置好扫描包路径的话,是不是就可以完成纯自动配置了。如图
我们的预期目标是只通过一个配置类就能完成全部默认配置,那么切入点就是这个类,如果我们能根据配置类所在的包自动设置扫描路径的话,是不是就可以实现"一类配置"了。、
但是问题来了,无论是RootConfig还是WebConfig,配置扫描包的路径都是通过@ComponentScan的basePackages属性来配置的,我们知道注解的参数信息是要提前写好的,应该说是constant,不变的,来看
意思就是说,在一个class加载之前,其注解信息必须是已知的,也就是不能在类加载期间动态设置,那怎么办呢?回过头来看我们的升华版配置流程,我们是不是只要在WebApplicationInitializer实现类调用getxxxClasses方法之前修改好配置类@ComponentScan的basePackages属性就行,那么就可以在配置类class加载之后再进行basePackages的修改是不是就可以了。这个办法的关键点就在于动态修改class注解属性是否可行,答案是可行的。
注解相关知识:
package java.lang.annotation;
/**
* The common interface extended by all annotation types. Note that an
* interface that manually extends this one does not define
* an annotation type. Also note that this interface does not itself
* define an annotation type.
*
* More information about annotation types can be found in section 9.6 of
* The Java™ Language Specification.
*
* The {@link java.lang.reflect.AnnotatedElement} interface discusses
* compatibility concerns when evolving an annotation type from being
* non-repeatable to being repeatable.
*
* @author Josh Bloch
* @since 1.5
*/
public interface Annotation {
正因为注解的作用是标注解释,所以这种特殊的接口不需要也不让开发者去实现,而是由jvm通过代理自动去实现,也正是因为这样,所以在定义注解的属性时,你定义的是有default缺省值的方法,而不是一个变量。也就是说jvm会自动去生成一个类去实现注解接口,而接口方法需要返回的值已经在编译前确定了,所以叫代理,这个代理自然就是Proxy JDK 动态代理了。
编写注解工具类
根据上述已知信息,有代理对象,就可用通过反射来修改属性值,ok,编写一个注解工具类AnnotationUtils,封装一个修改class注解属性的方法,其中NullAnnotationException是自定义异常。
public abstract class BaseAnnotationUtils {
public BaseAnnotationUtils() {
}
private static final String FIELD_MENMBER = "memberValues";
/**
* 修改class注解属性
* @param targetClass 目标class
* @param aClass 注解class
* @param fieldName 预修改的注解的属性名称
* @param fieldValue 预修改的注解的属性值
* @param 注解类型
*/
public static <A extends Annotation> void changeAnnotaionField(Class<?> targetClass, Class<A> aClass, String fieldName, Object fieldValue)
throws NullAnnotationException, NoSuchFieldException, IllegalAccessException {
A annotation = targetClass.getAnnotation(aClass);
if (annotation == null) {
// 找不到注解则抛出异常
throw new NullAnnotationException("<" + aClass.getName() + "> was not found on [" + targetClass.getName() + "]");
}
// 调用处理器,每一个被代理的实例都有一个调用处理器
InvocationHandler annotationInvocationHandler = Proxy.getInvocationHandler(annotation);
// 获取代理对象memberValues属性的属性对象
Field field = annotationInvocationHandler.getClass().getDeclaredField(FIELD_MENMBER);
// 打破私有
field.setAccessible(true);
// 获取代理对象的memberValues属性值
Map<String,Object> memberMap = (Map<String, Object>) field.get(annotationInvocationHandler);
// 重新设置值
memberMap.put(fieldName,fieldValue);
}
}
随便在一个类里编写main方法,测试成功!
RootConfig类
访问修饰符改为包内可访问,对其它项目来说,这个类不需要对外开放;
扫描配置增加过滤器,不扫描WebConfig和Controller,思考一下spring+springmvc上下文及父子容器问题,这样做比较优雅。
@ComponentScan(excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = WebConfig.class),
@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)
})
@Configuration
class RootConfig {
}
WebConfig类
访问修饰符改为包内可访问,对其它项目来说,这个类不需要对外开放;
指定包含Controller的类,加不加效果都一样
@ComponentScan(includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)})
@EnableAspectJAutoProxy
@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
}
web容器类
改成抽象类,其它地方继承使用即可;
添加自动修改RootConfig类和WebConfig类的@ComponentScan注解的basePackages属性的代码。
public abstract class BaseSpringMvcDefaultConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
{
// 构造结束后自动获取当前类所在包,并修改RootConfig和WebConfig的class的@ComponentScan注解属性
String basePackage = this.getPackageName();
try {
appointComponentScanBasePackage(new String[]{basePackage});
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
/**
* 指定扫描基本包路径
* @param basePackages
* @throws NoSuchFieldException
* @throws IllegalAccessException
* @throws NullAnnotationException
*/
private static void appointComponentScanBasePackage(String[] basePackages) throws NoSuchFieldException, IllegalAccessException, NullAnnotationException {
String paramName = "basePackages";
BaseAnnotationUtils.changeAnnotaionField(RootConfig.class, ComponentScan.class,paramName,basePackages);
BaseAnnotationUtils.changeAnnotaionField(WebConfig.class, ComponentScan.class,paramName,basePackages);
}
/**
* 获取当前对象所属类所在包名
* @return
*/
private String getPackageName() {
return this.getClass().getPackage().getName();
}
/**
* 相当于添加了Spring的容器
* @return
*/
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{RootConfig.class};
}
/**
* 相当于添加了SpringMVC的容器
* @return
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
/**
* 相当于配置了DispatcherServlet的 /
* @return
*/
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
此时可用版本已经升级为封装版本,把封装版本删除web相关的包和webapp后打包为jar包,并install到本地仓库。
新建demo测试
导包
自定义配置类,继承SpringMVC的缺省配置,无需再做任何操作,注意这里也没有加什么注解,只有继承!!!
public class MyConfiguration extends BaseSpringMvcDefaultConfig {
}
测试用的controller和service
@Controller
public class DemoController {
@Autowired
DemoService service;
@RequestMapping("/demo/test")
@ResponseBody
public String sayDemo() {
return service.getDemoString();
}
}
@Service
public class DemoService {
public String getDemoString() {
return "This Demo!";
}
}
WebApplicationInitializer的超类实现类AbstractAnnotationConfigDispatcherServletInitializer有一个方法,getServletFilter(),是可以添加过滤器的,但是默认过滤的是"/"路径,也就是dispatcherServlet的匹配路径,自定义过滤路径貌似没法生效,暂且可以重写WebApplicationInitializer的onStartup方法,先添加过滤器再初始化。
单元测试模块本来尝试也做成default配置一样,继承即可使用,但发现并没有那么简单,测试的话,跟原先的mvc一样,写个配置类扫描全部bean,用@RunWith(SpringJUnit4ClassRunner.class)和@ContextConfiguration完成即可