xml转config避坑

为啥要转成java_config配置

普通的springmvc项目一般是xml结合注解配置:在web.xml中设置DispatcherServlet启动入口扫描各种xml,在xml中开启注解配置(<context:annotation-config/>),我们就可以用@Service,@Controlle...。

然鹅这种配置文件+注解的方式会让人觉得臃肿,看起来也很乱,如果项目能转成纯java配置,无疑令人赏心悦目(可能有代码洁癖)。还有重要的一点是本人感觉采用java config的方式扩展性要强一些,无论是spring cglib增强(配置了@Configuration)还是spring新特性(自动装配)或者是将来升级为springboot,写更少的代码完成更多地功能特性,java config模式都能做更多的事情。

还有其他补充的可以继续往下吹。。。

xml转java config(以auth项目为例)

1.解放web.xml

what? web.xml这么重要的文件也能去掉???

是的,借助于servlet3.0的一个特性:当容器启动时容器会在类路径查找ServletContainerinitializer接口的实现类,如果发现这样的类就用用这个类来配置Servlet容器,Spring web框架中存在一个类SpringServletContainerInitializer,它实现了上述接口:

public class SpringServletContainerInitializer implements ServletContainerInitializer {
    public SpringServletContainerInitializer() {
    }

    public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
        ...
        部分代码省略
        ...
        while(var4.hasNext()) {
            WebApplicationInitializer initializer = (WebApplicationInitializer)var4.next();
            initializer.onStartup(servletContext);
        }
        ...
        部分代码省略
        ...
    }
}

在这个类中的onStartup方法中,会查找实现了WebApplicationInitializer接口的类,因此,只要 实现了它或者继承它的子类AbstractAnnotationConfigDispatcherServletInitializer,我们就可以配置Servlet上下文啦~

首先,我们看下原有的web.xml配置文件都有啥:

<web-app>
    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <servlet>
        <servlet-name>spring</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
           <param-name>contextConfigLocation</param-name>-->
           <param-value>classpath:applicationContext-*.xml</param-value>-->
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>spring</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

有Servlet,Filter...,这些都是通过xml标签配置的,我们的目的就是用代码替换这些配置,首先我们先写一个类,继承自AbstractAnnotationConfigDispatcherServletInitializer(或者实现WebApplicationInitializer接口):

public class AuthWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{AuthRootAppConfig.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{AuthWebAppConfig.class};
    }

    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    @Override
    protected Filter[] getServletFilters() {
        return super.getServletFilters();
    }

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        servletContext.addFilter("targetFilterLifecycle", new CharacterEncodingFilter("UTF-8", true))
                .addMappingForUrlPatterns(null, false, "/*");
        servletContext.addFilter("shiroFilter", SessionFilter.class).addMappingForUrlPatterns(null, false, "/*");
        super.onStartup(servletContext);
    }
}

如上述代码,我新建了一个AuthWebAppInitializer类,继承了AbstractAnnotationConfigDispatcherServletInitializer,它有很多个可以重写的方法:

  • getRootConfigClasses(): 配置RootApplicationContext

  • getServletConfigClasses():配置springmvc的contenxt(DispatcherServlet)

  • getServletMappings():对应web.xml中的<servlet>标签

  • getServletMappings():DispatcherServlet拦截的规则

  • getServletFilters():和DispatcherServlet绑定的过滤器

说明:在上述文件web.xml中配置的filter,其实并没有和dispatcherServlet有任何关联,所以需要将其配置到onStartup()方法中,将filter放入servletContext中

那getRootConfitClasses()和getServletConfigClasses()是啥?这个会在下一步中解答

这样,我们替换web.xml配置文件就完成啦,不过仅仅这样还不可以,因为在打包的时候,如果springmvc项目没有web.xml文件会报错,不信的自己可以试下,保证100%复现:

Failed to execute goal org.apache.maven.plugins:maven-war-plugin:2.2:war (default-war) on project auth-web: Error assembling WAR: webxml attribute is required (or pre-existing WEB-INF/web.xml if executing in update mode)

我们需要引入一个maven编译插件,忽略对web.xml文件的检测:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-war-plugin</artifactId>
    <version>2.3</version>
    <configuration>
        <!--忽略对web.xml文件的检查-->
        <failOnMissingWebXml>false</failOnMissingWebXml>
    </configuration>
</plugin>

到此为止,就可以干脆的delete掉web.xml文件啦~~

2.解放spring*.xml

我们经常会看到在web.xml文件中作如下配置:

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <servlet>
        <servlet-name>dispatch</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
             <!--springMVC相关-->
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:dispatchServlet.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatch</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

我们经常通过这样的配置,先加载listerner相关配置,扫描spring基础配置,然后再加载dispatcherServlet去初始化springmvc相关信息(两者关系可自行google~)

切换到java config,我们自己实现的AuthWebAppInitializer类,其中

  • getRootConfigClasses():基础配置

  • getServletConfigClasses():DispatcherServlet

这两个方法可以让我们手动配置自己的初始化方法,在getRootConfigClasses()中,我们可以配置加载spring的基础信息,比如开启apollo,扫描dao,service等:

@EnableApolloConfig  // 开启apollo
public class AuthRootAppConfig {
}

在getServletConfigClasses()中,配置springmvc相关的配置(拦截器,视图解析器等):

@EnableWebMvc
public class AuthWebAppConfig extends WebMvcConfigurerAdapter {
    ...
    省略部分代码
    ...
    /**
    * 配置JSP视图解析器
    *
    * @return
    */
    @Bean
    public InternalResourceViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/views/");
        viewResolver.setSuffix(".jsp");
        viewResolver.setViewClass(JstlView.class);
        // 可以在JSP页面中通过${}访问beans
        viewResolver.setExposeContextBeansAsAttributes(true);
        return viewResolver;
    }

    //添加拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authControllerInterceptor).addPathPatterns("/**");
        registry.addInterceptor(authServerInterceptor).addPathPatterns("/**");
        super.addInterceptors(registry);
    }

    //添加参数解析器
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(authParamResolver);
        super.addArgumentResolvers(argumentResolvers);
    }

    //资源过滤
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/images/**").addResourceLocations("/images/");
        registry.addResourceHandler("/css/**").addResourceLocations("/css/");
        super.addResourceHandlers(registry);
    }

    ...
    省略部分代码
    ...
}

这样,通过spring的root ApplicationContext和 Web ApplicationContext,之前spring配置的xml文件就可以解放啦~

3.解放application-*.xml

除了spring基础配置和webmvc配置外,可能还有些配置信息遗留在xml,比如说数据库连接信息,redis配置信息等等,剩下的这些配置迁移就简单多啦~

我们可以在基础包下新建一个config目录(叫啥名都行),然后再此包下新建各种Config类,比如说数据库的就DataBaseConfig,redis的就redisConfig(名称啥的都看个人习惯啦),然后直接java config就行啦,举个例子:

@Configuration
public class DataSourceConfig {

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String userName;

    @Value("${jdbc.password}")
    private String password;

    @Bean("datasourceAuth")
    public DataSource datasourceAuth() {
        BasicDataSource basicDataSource = new BasicDataSource();
        basicDataSource.setDriverClassName("com.mysql.jdbc.Driver");
        basicDataSource.setUrl(url);
        basicDataSource.setUsername(userName);
        basicDataSource.setPassword(password);
        return basicDataSource;
    }

}

上述代码配置了数据库信息,代替了xml文件中的<bean id='datasource' class=‘xxx’ .../>,用@Configuration注解表明此类是个配置类,需要被解析,但是spring不清楚它在哪里,所以需要我们在Root ApplicationContext中配置扫描路径@ComponentScan。

这样,把xml文件中剩余的信息都以类似这种方式迁移过来就ok啦,跑跑试试吧~

还可以进化

上面配置完成后我们其实已经完成了xml转java config的工作,其实我们还可以抛弃tomcat(不是真的抛弃,是引入插件,不需要再打包之后再扔到tomcat运行了,因为天亮了就先不写了~)

Tips

1.spring派生注解

能被spring扫描到的注解有很多,比如说@Component,@Service,@Controller,@Configuration,@Repository...这些个注解都是被@Component派生出来的(在其注解上包含@Component),派生出来就是想不同的地方有不同的区分,比如在controller层用@Controller,service层用@Service等,其实作用都是能够被spring扫描到

2.Full模式 & Litle模式

上述我们在配置数据库相关信息时,用的是@Configuration,被这个注解的类会被spring当做一个配置类,相当于一个xml配置文件,同时它还是个Full模式的注解,即:被spring扫描到配置类被@Configuration修饰时,会将其用enhancer增强,就是用cglib生成其代理类(听说在这个配置类下的@Bean可以是private/final 或者static的,不过应该没有这么干的),如果从网上搜索下spring full模式,大多都是说用cglib代理过后提高运行性能巴拉巴拉:

image.png

其实,它还有个隐藏的特性,是网上搜不到的~

敲黑板!!!这个真的从网上搜不到~

在spring容器初始化时,会将ConfiturationClassPostProcessor这个类注入到spring容器中,目的是帮助spring容器初始化,比如说扫描解析各种注解(具体可查看spring源码)。当发现被扫描到的类被@Configuration修饰时,会调用enhancer对象进行增强:

public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor, PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {
    ...
    省略部分代码
    ...

    //对@Configuration标记的类进行enhancer增强
    public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory) {
        ...
        省略部分代码
        ...

        Class<?> configClass = beanDef.resolveBeanClass(this.beanClassLoader);
        Class<?> enhancedClass = enhancer.enhance(configClass, this.beanClassLoader);

        ...
        省略部分代码
        ...
    }

    ...
    省略部分代码
    ...
}

可以看到在方法中调用了ConfigurationClassEnhancer对象的enhance方法:

class ConfigurationClassEnhancer {
    ...
    省略部分代码
    ...

    public Class<?> enhance(Class<?> configClass, ClassLoader classLoader) {
        ...
        省略部分代码
        ...

        Class<?> enhancedClass = this.createClass(this.newEnhancer(configClass, classLoader));

        ...
        省略部分代码
        ...

        return enhancedClass;

    }

    private Enhancer newEnhancer(Class<?> configSuperClass, ClassLoader classLoader) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(configSuperClass);
        enhancer.setInterfaces(new Class[]{ConfigurationClassEnhancer.EnhancedConfiguration.class});
        enhancer.setUseFactory(false);
        enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
        enhancer.setStrategy(new ConfigurationClassEnhancer.BeanFactoryAwareGeneratorStrategy(classLoader));
        enhancer.setCallbackFilter(CALLBACK_FILTER);
        enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes());
        return enhancer;
    }

    public interface EnhancedConfiguration extends BeanFactoryAware {
    }
    ...
    省略部分代码
    ...
}

重点来了:在这个enhance方法中,调用了newEnhancer方法创建Enhancer对象,我们可以看到在创建enhancer对象时,set了一个接口ConfigurationClassEnhancer.EnhancedConfiguration.class,也就是说我们将来拿到的这个对象,继承了这个接口,那这个接口是干嘛的呢?它也在当前类中被定义出来,我们可以看到这个接口继承了BeanFactoryAware,是不是瞬间明白了些(不懂Aware的可以自行google)

总结下:被@Configuration标注的类,在被扫描到时会被spring容器进行增强,就是通过enhancer生成Class对象,而且此对象继承了Aware接口,拥有了获取bean容器的能力,在之后spring初始化bean时(调用getBean())会将容器对象注入到该对象中,使得该对象拥有了感知spring容器的功能。

思考题:为什么spring要对@Configuration如此兴师动众?欢迎解答~

未完待续。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容