Spring+SpringMVC无xml自动配置详解

本篇目的:

  • 介绍Spring+SpringMVC无xml配置的方法,简化开发,后续其它组件的配置方式跟SpringBoot是差不多的。
    SpringBoot是很好, 但是技术归技术,商业归商业,有时候并不是所有人都能用上SpringBoot,公司产品的要求决定了使用了技术栈,那也是没办法的事。
  • 个人愚见,从开发的角度看,能用SpringBoot就用SpringBoot,优先选择。从学习角度看,深入Spring+SpringMVC对个人的提高在SpringBoot上也能用到,因为SpringBoot本身就是Spring+SpringMVC的结合体

梳理一下Spring+SpringMVC与SpringBoot:

  1. SpringBoot ≥ Spring+SpringMVC;SpringBoot囊括了Spring+SpringMVC的所有并内置Tomcat,提供统一配置。SpringBoot对开发者来说最直接的好处就是,不用自己写那些重复的xml配置,打war包,部署war到tomcat。
  2. 特别让刚开始接触Spring的开发人员最烦的就是,使用Spring+SpringMVC开发时,用IDEA各种配置Tomcat、编译打包,老是因为目录问题配置问题啥的,各种报错,各种启动失败的问题,积极性直接被打击。
  3. SpringBoot凭借集成配置与工程聚合的优点成为JavaWeb开发的主流,不得不说SpringBoot确实降低了Spring的使用门槛,也是新手快速上手Spring框架的选择,结合当下微服务与快速开发的理念,SpringBoot无疑是很好的选择。

一、Spring+SpringMVC无xml配置原理

预期效果:
生成一个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容器中的执行原理与配置过程,我们可以大致分为以下几步:

  1. 在web.xml中配置DispatcherServlet
  2. 将Service、DAO层的bean配置在spring配置文件中,暂且叫它rootConfig
  3. 在springmvc配置文件中配置Controller层的bean以及开启MVC相关配置,暂且叫它webConfig

AbstractAnnotationConfigDispatcherServletInitializer实现了WebApplicationInitializer接口,现在我们只需要继承AbstractAnnotationConfigDispatcherServletInitializer抽象类实现以下三个方法即可代替以上步骤。

2.1 配置类编写

创建maven项目如下

  • BaseSpringMvcDefaultConfig = web.xml(暂且称为web容器类)
  • RootConfig = SpringContext.xml(Spring上下文)*
  • WebConfig = SpringMVC-config.xml(SpringMVC上下文)

Spring+SpringMVC无xml自动配置详解_第1张图片
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包

  • 继承WebMvcConfigurationSupport类重写方法实现可以各种配置,这一点和SpringBoot是一样的,如重写addResourceHandlers方法配置静态资源路径。(关于WebMvcConfigurationSupport,可参考这一篇:SpringMVC使用Java类进行配置)
  • @EnableWebMvc:开启springmvc相关的配置,如xml配置里的
  • @EnableAspectJAutoProxy:开启AOP自动代理
@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);
    }

}

这三个类可称为“配置三剑客”

2.2 编写controller验证测试

Spring+SpringMVC无xml自动配置详解_第2张图片

@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!";
    }
}

Spring+SpringMVC无xml自动配置详解_第3张图片
测试ok,可用版本出炉,但仔细想想,如果每个项目都这样配置,都要复制3个配置类,每次都要指定包扫描路径,还是过于重复了,所以还没达到我们预期的目标

三、升级配置(封装版本)

3.1 升级思路

先来看一下可用版本配置的执行流程,
Spring+SpringMVC无xml自动配置详解_第4张图片

很显然上述配置关键入口便是扫描包的路径,思考一下,程序启动时,第一步扫描WebApplicationInitializer实现类是系统自动完成的,那么如果我们能在第二步之前设置好扫描包路径的话,是不是就可以完成纯自动配置了。如图
Spring+SpringMVC无xml自动配置详解_第5张图片
我们的预期目标是只通过一个配置类就能完成全部默认配置,那么切入点就是这个类,如果我们能根据配置类所在的包自动设置扫描路径的话,是不是就可以实现"一类配置"了。、
但是问题来了,无论是RootConfig还是WebConfig,配置扫描包的路径都是通过@ComponentScanbasePackages属性来配置的,我们知道注解的参数信息是要提前写好的,应该说是constant,不变的,来看
Spring+SpringMVC无xml自动配置详解_第6张图片

意思就是说,在一个class加载之前,其注解信息必须是已知的,也就是不能在类加载期间动态设置,那怎么办呢?回过头来看我们的升华版配置流程,我们是不是只要在WebApplicationInitializer实现类调用getxxxClasses方法之前修改好配置类@ComponentScan的basePackages属性就行,那么就可以在配置类class加载之后再进行basePackages的修改是不是就可以了。这个办法的关键点就在于动态修改class注解属性是否可行,答案是可行的。

3.2 动态修改class的注解属性

注解相关知识:

  1. 保留策略为 RUNTIME 的注解在运行期是保留的。
  2. 出于某些技术原因,Java 虚拟机使用的“真实”注释类的实例是动态代理的实例。 什么意思?我们知道声名注解关键字为“@interface”,看着跟接口大差不差,其实注解就是一种特殊的接口Annotation接口是所有注解的超级父接口,好比所有的引用类型都有个爹叫Object,
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 动态代理了。

  1. Java 注解的代理类有一个名为 memberValues 的私有Map,其中存储了属性名称和属性值的k-v对。也就是说你在注解声名的方法名和返回值都存在这个map里

编写注解工具类
根据上述已知信息,有代理对象,就可用通过反射来修改属性值,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方法,测试成功!

Spring+SpringMVC无xml自动配置详解_第7张图片

3.3 “配置三剑客”升级

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到本地仓库。
Spring+SpringMVC无xml自动配置详解_第8张图片
新建demo测试
Spring+SpringMVC无xml自动配置详解_第9张图片
导包
Spring+SpringMVC无xml自动配置详解_第10张图片

自定义配置类,继承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!";
    }

}

测试ok!
Spring+SpringMVC无xml自动配置详解_第11张图片

关于过滤器的添加

WebApplicationInitializer的超类实现类AbstractAnnotationConfigDispatcherServletInitializer有一个方法,getServletFilter(),是可以添加过滤器的,但是默认过滤的是"/"路径,也就是dispatcherServlet的匹配路径,自定义过滤路径貌似没法生效,暂且可以重写WebApplicationInitializer的onStartup方法,先添加过滤器再初始化。

关于单元测试

单元测试模块本来尝试也做成default配置一样,继承即可使用,但发现并没有那么简单,测试的话,跟原先的mvc一样,写个配置类扫描全部bean,用@RunWith(SpringJUnit4ClassRunner.class)和@ContextConfiguration完成即可

你可能感兴趣的:(Spring,SpringMVC深入,spring,java,编程语言,经验分享,后端)