zoukankan      html  css  js  c++  java
  • Spring中你可能不知道的事(一)

    Spring作为Java的王牌开源项目,相信大家都用过,但是可能大家仅仅用到了Spring最常用的功能,Spring实在是庞大了,很多功能可能一辈子都不会用到,今天我就罗列下Spring中你可能不知道的事。一是可以帮助大家以后阅读源码,知道Spring为什么会这么写,二是可以作为知识储备,当人家不会的时候,你正好知道这个点,三下五除二就搞定了,嘿嘿。三是平时吹牛的时候可以更有资本。。。当然最重要的就是可以对Spring有一个更全面的认识。

    register

    现在官方推荐应该就是用JavaConfig的风格来完成Spring的配置,也是现在的主流用法。我们经常这么写:

    @Configuration
    @ComponentScan
    public class AppConfig {
    }
    
    AnnotationConfigApplicationContext context=new AnnotationConfigApplicationContext(AppConfig.class);
    

    这段代码太简单,就不再解释了,但是我们可以把方法拆分下:

    AnnotationConfigApplicationContext context=new AnnotationConfigApplicationContext();
    context.register(AppConfig.class);
    

    在第二行代码才去注册配置类。

    效果是一样的,我们除了可以注册配置类,还可以单独注册一个 bean:

    @Component
    public class Service {
    }
    
    public class Main {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
            context.register(Service.class);
            context.refresh();//很重要        
            System.out.println(context.getBean(Service.class).getClass().getSimpleName());
        }
    }
    

    这样我们就可以完成对bean的注入,这里面有一个细节很重要,需要调用refresh方法,不然会报错:

    public class Main {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
            context.register(Service.class);
            System.out.println(context.getBean(Service.class).getClass().getSimpleName());
        }
    }
    

    image.png

    registerBean

    上面的方法虽然可以单独注册一个bean,但是在bean的类上,你必须打上@Component或者@Service或者@Repository,如果你不想用默认的作用域,也得打上@Scope,有没有一种方法,可以不用在bean的类上打各种注解?此时registerBean出场了:

    public class Service {
        public Service(String str){
            System.out.println(str);
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
            context.registerBean("myService", Service.class, () -> new Service("Hello"), z -> {
                z.setScope("prototype");
            });
            context.refresh();
            System.out.println(context.getBean("myService").getClass().getSimpleName());
            System.out.println(context.getBeanDefinition("myService").getScope());
        }
    }
    

    我注册了名为myService的Bean,类是Service,并且作用域为prototype,且会调用带参的构造方法:

    image.png

    BeanPostProcessor

    如果说上面两个小点不重要,那么这一个就是重磅级的了,BeanPostProcessor是Spring扩展点之一,BeanPostProcessor是一个接口,程序员可以通过实现它,插手bean的实例化过程,在bean创建前后做一些事情。在Spring内部,也大量的运用了BeanPostProcessor来完成各种功能。我们可以看下Spring内部有多少类实现了BeanPostProcessor接口(注意,注意,前方高能)。

    image.png

    Spring内部有这么多类(间接)实现了BeanPostProcessor接口,可想而知这个接口的重要性,那么这个接口应该怎么使用呢,很简单,我们只需要写一个类去实现BeanPostProcessor接口就可以。

    在这里,我利用这个接口,来完成一个阉割版的JDK动态代理的注入:

    首先定义一个接口:

    public interface Service {
        void query();
    }
    

    实现类:

    @Component
    public class ServiceImpl implements  Service {
        @Override
        public void query() {
            System.out.println("正在查询中");
        }
    }
    

    实现InvocationHandler接口:

    public class MyInvationHandler implements InvocationHandler {
        private Object target;
    
        public MyInvationHandler(Object target){
            this.target=target;
        }
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("进来了");
            Object obj = method.invoke(target, args);
            System.out.println("出去了");
            return obj;
        }
    }
    

    实现BeanPostProcessor 接口:

    @Component
    public class MyBeanPostProcess implements BeanPostProcessor {
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            Object o = Proxy.newProxyInstance(MyBeanPostProcess.class.getClassLoader(),
                    bean.getClass().getInterfaces(), new MyInvationHandler(bean));
            return o;
        }
    
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            return bean;
        }
    }
    

    配置类

    @Configuration
    @ComponentScan
    public class AppConfig {
    }
    

    测试方法:

    public class Main {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
            context.getBean(Service.class).query();
        }
    }
    

    运行结果:

    image.png

    有木有很神奇,不管在main方法,还是业务的实现类,都没有看到JDK动态代理的影子,但是动态代理真真实实生效了,这就是BeanPostProcessor接口的神奇所在,事实上,Spring内部也是通过实现BeanPostProcessor接口来完成动态代理的,这个暂时不表。

    BeanFactoryPostProcessor

    BeanFactoryPostProcessor也是Spring的扩展点,程序员可以通过实现它,读取bean的定义,然后对其进行修改,比如我需要修改bean的作用域为prototype,可以这么做:

    @Component
    public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException {
            factory.getBeanDefinition("repo").setScope("prototype");
        }
    }
    
    @Repository
    public class Repo {
    }
    
    public class Main {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
            System.out.println(context.getBeanDefinition("repo").getScope());
        }
    }
    

    image.png
    大家都知道bean的默认作用域为singleton,这里就通过实现BeanFactoryPostProcessor接口,把作用域改成了prototype。

    BeanFactoryPostProcessor 在 BeanPostProcessor之前。

    单例bean中有原型bean

    如果一个单例的bean中,包含原型的bean,会发生什么事情呢?我们写一个例子看一下:

    @Configuration
    @ComponentScan
    public class AppConfig {
        @Bean
        @Scope("singleton")
        public Single singleton(){
            return new Single();
        }
        @Bean
        @Scope("prototype")
        public Prototype prototype(){
            return new Prototype();
        }
    }
    
    public class Single {
        public Single(){
            System.out.println("Single构造方法");
        }
        @Autowired
        private Prototype prototype;
        public Prototype getPrototype() {
            return prototype;
        }
        public void setPrototype(Prototype prototype) {
            this.prototype = prototype;
        }
        public void say() {
            System.out.println(this);
            prototype.say();
        }
    }
    
    public class Prototype {
        public Prototype(){
            System.out.println("Prototype构造方法");
        }
        public void say() {
            System.out.println(this);
        }
    }
    
    @Component
    public class Test {
        @Autowired
        Single single;
        public void run() {
            for (int i = 0; i < 5; i++) {
                single.say();
            }
        }
    }
    

    因为代码比较长,避免大家上下来回滚动,我简单的说明下这段代码:Single类是单例的,Prototype是原型的,Single类依赖Prototype,分别给两个类添加一个构造方法,打印一句话,Single类中的方法调用Prototype类的方法,两个方法都打印this。然后再测试方法中自动注入Single,循环5次,调用Single类中的方法。

    运行结果:
    image.png

    这结果明显有问题,Single因为是单例的,只能执行到一次构造方法,每次打印出来的对象也相同,这是没有问题的,但是Prototype是原型的,也只运行了一次构造函数,打印出来的对象也相同,这就有问题了。

    这问题怎么解决呢?

    ApplicationContextAware

    对Single类进行改造,让它实现ApplicationContextAware接口中的setApplicationContext方法:

    public class Single implements ApplicationContextAware {
        public Single() {
            System.out.println("Single构造方法");
        }
    
        private ApplicationContext context;
    
        public void say() {
            System.out.println(this);
            context.getBean(Prototype.class).say();
        }
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            this.context = applicationContext;
        }
    }
    

    运行结果:

    image.png

    说的简单点,就是通过ApplicationContextAware接口中的setApplicationContext方法,获得ApplicationContext ,赋值给类中的变量ApplicationContext context, 然后从context中获得Prototype Bean。

    此方法需要依赖ApplicationContext。

    lookup

    @Component
    @Scope("singleton")
    public class Single {
        public Single() {
            System.out.println("Single构造方法");
        }
        public void say() {
            System.out.println(this);
            getPrototype().say();
        }
        @Lookup
        public Prototype getPrototype() {
            return null;
        }
    }
    
    @Component
    @Scope("prototype")
    public class Prototype {
        public Prototype(){
            System.out.println("Prototype构造方法");
        }
    
        public void say() {
            System.out.println(this);
        }
    }
    

    运行结果:

    image.png

    此方法需要把配置类中的定义bean改为在类上加注解的方式。

    Import

    Import是Spring提供的注解,可以通过这个注解,在一个类引入另外一个类, 并且自动完成另外一个类的注册:

    @Configuration
    @Import(ServiceImpl.class)
    public class AppConfig {
    }
    
    public class ServiceImpl  {
        public void query() {
            System.out.println("正在查询中");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
            context.getBean(ServiceImpl.class).query();
        }
    }
    

    运行结果:

    image.png

    可以看到虽然ServiceImpl类上没有打上任何注解,但是在AppConfig配置类上通过Import注解,把ServiceImpl给引入进来了,并且自动注册了ServiceImpl。

    也许,单单使用Import注解,会把代码搞得更复杂,所以需要搭配使用,才能把它的能力发挥出来,下面让我们有请ImportSelector。

    ImportSelector

    让我们把目光回到介绍BeanPostProcessor的这一段中,在其中,我们定义了一个MyBeanPostProcess来完成JDK动态代理,但是让我们想一个问题,如果我们不需要使用这个MyBeanPostProcess了,怎么办?我们需要把MyBeanPostProcess类上的Component注解删除,哪天又需要使用了,还得加上,如果只有一个类,还不算糟糕,但是如何有几十个类呢?相当麻烦,我们能不能在一个类中统一处理,需要启动哪些Bean,就像Spring Boot 一样?当然可以。我们可以借助于ImportSelector来完成:

    首先我们需要定义一个类,实现ImportSelector 中的
    selectImports方法,这个方法返回的是需要与此类绑定的bean的名称的数组:

    public class AspectSelector implements ImportSelector {
        @Override
        public String[] selectImports(AnnotationMetadata importingClassMetadata) {
            return new String[]{MyBeanPostProcess.class.getName()};
        }
    }
    

    我们再自定义一个注解,打上Import注解,引入上面的类:

    @Import(AspectSelector.class)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface EnableAspect{
    }
    

    注意看AppConfig 的注解,多了一个EnableAspect注解:

    @Configuration
    @ComponentScan
    @EnableAspect
    public class AppConfig {
    }
    
    public class Main {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
            context.getBean(Service.class).query();
        }
    }
    

    然后我们把MyBeanPostProcess上的注解删除,运行:

    image.png

    当我们不需要使用MyBeanPostProcess了,只要在AppConfig删除EnableAspect注解就OK了。

    这是相当炫酷的一个技巧,在SpringBoot大量使用,比如开启事务管理EnableTransactionManagement。

    FactoryBean

    FactoryBean经常会和BeanFactory放在一起比较,因为他们太像了,不过仅仅是长得像,其实它们完全不是同一个东西。

    FactoryBean,是一种特殊的Bean,特殊在它除了自身是Baen,还可以生产Bean,是不是很符合FactoryBean这个名称?

    FactoryBean是一个接口,我们需要实现它:

    @Component
    public class MyFactoryBean implements FactoryBean {
    
        public Object getObject() throws Exception {
            return new DataSource();
        }
        @Override
        public Class<?> getObjectType() {
            return null;
        }
    }
    
    public class DataSource {
    }
    
    @Configuration
    @ComponentScan
    public class AppConfig {
    }
    
    public class Main {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
            System.out.println(context.getBean("myFactoryBean").getClass().getSimpleName());
            System.out.println(context.getBean("&myFactoryBean").getClass().getSimpleName());
        }
    }
    

    运行结果:

    image.png

    我们可以看到MyFactoryBean上打了一个Component,它可以被扫描到,但是DataSource上什么都没有加,按理来说,是没有被扫描到的,但是它就是被注册进去了,因为它实现了FactoryBean接口,在getObject方法返回了DataSource的实例,可以理解为DataSource是MyFactoryBean生产出来的一个Bean。

    让我们仔细看下main方法和运行结果,可以看到 MyFactoryBean本身的BeanName是&myFactoryBean,MyFactoryBean生产出来的Bean的BeanName是myFactoryBean。

    这有什么用呢?可以隐藏构建Bean的细节。如果我们的DataSource是第三方提供的,里面有一堆的字段需要配置,还有一堆的依赖,如果我们来配置的话,根本无法完成,最好的办法就是还是交给维护第三方去配置,但是DataSource是不能去修改的。这个时候,就可以用FactoryBean来完成,在getObject配置好DataSource,并且返回。我们经常使用的Mybatis也利用了FactoryBean接口。

    Spring实在是太庞大了,很多功能都不是经常用,我在这里只是稍微罗列了几个小点,加上我们经常用的那些,可能还不及Spring的十分之一,这已经是乐观的了。

    限于篇幅关系,这一章的内容到这里就结束了,其中BeanPostProcessor,BeanFactoryPostProcessor,FactoryBean,Import,ImportSelector这几块内容非常重要,正在由于这些,才让Spring变的更加灵活,更加好用。

  • 相关阅读:
    Recommended Books for Algo Trading in 2020
    Market Making is simpler than you think!
    Top Crypto Market Makers of 2020
    Top Crypto Market Makers, Rated and Reviewed
    爬取伯乐在线文章(五)itemloader
    爬取伯乐在线文章(四)将爬取结果保存到MySQL
    爬取伯乐在线文章(三)爬取所有页面的文章
    爬取伯乐在线文章(二)通过xpath提取源文件中需要的内容
    爬取伯乐在线文章(一)
    爬虫去重策略
  • 原文地址:https://www.cnblogs.com/CodeBear/p/10275845.html
Copyright © 2011-2022 走看看