如何假装成一个服务端开发(一)java
如何假装成一个服务端开发(二)android
如何假装成一个服务端开发(三) spring
首先,咱们抛开Spring 或者 服务端开发,来聊聊这两个概念。app
IoC和面向对象设计同样,从本质上来讲是一种概念。而这种概念的主要目的就在于对象之间的解耦。在原来的逻辑里面,假如咱们有一个User类,User类中须要使用UserInfo类,而后咱们还有一个Client类。框架
如今我须要在Client中使用User,那么我首先须要在Client类中new 一个User类, 而后再new 一个UserInfo类,最后吧UserInfo,注入到User类中。这个时候,咱们发现Client类耦合了User类,和UserInfo类。ide
因此聪明的咱们决定下降耦合,既然UserInfo是在User类内部的,那么咱们能够在User类内部本身建立UserInfo。这样的话,Client类就和UserInfo解耦了。不过引入的问题就是User类又和UserInfo类耦合了。函数
因此怎么办,要不咱们建立一个中央平台吧,咱们在中央平台中建立User,在中央平台中建立UserInfo。当Client须要User的时候,直接去中央平台获取这个User便可。当User须要UserInfo时,也直接去中央平台获取这个UserInfo。这样的话,那么Client,User,UserInfo相互之间就那么耦合了。工具
这种思想就是所谓的IOC,而这个中央平台就是所谓的容器,这种技术就是所谓的DI。学习
这实现的过程当中,原来,咱们须要在Client中主动去建立对象,这是一个主动的过程,而使用中央平台以后,中央平台负责了对象的建立(固然高级的中央平台还会帮你管理这些对象的生命周期)。Client中若是须要使用一个对象,直接告诉中央平台,中央平台吐给你结果,这过程当中咱们没有主动去建立。ui
ok,以上是网上的权威说法,可是……这TM也算反转?总以为多少有些牵强啊喂。
再举个简单的例子,咱们是如何找女友的?常见的状况是,咱们处处去看哪里有长得漂亮身材又好的mm,而后打听她们的兴趣爱好、qq号、电话号、ip号、iq号………,想办法认识她们,投其所好送,这个过程是复杂深奥的,咱们必须本身设计和面对每一个环节。传统的程序开发也是如此,在一个对象中,若是要使用另外的对象,就必须获得它(本身new一个,或者从JNDI中查询一个),使用完以后还要将对象销毁(好比Connection等),对象始终会和其余的接口或类藕合起来。
那么IoC是如何作的呢?有点像经过婚介找女友,在我和女友之间引入了一个第三者:婚姻介绍所。婚介管理了不少男男女女的资料,我能够向婚介提出一个列表,告诉它我想找个什么样的女友,好比长得像李嘉欣,身材像林熙雷,唱歌像周杰伦,速度像卡洛斯,技术像齐达内之类的,而后婚介就会按照咱们的要求,提供一个mm,咱们只须要去和她谈恋爱、结婚就好了。简单明了,若是婚介给咱们的人选不符合要求,咱们就会抛出异常。整个过程再也不由我本身控制,而是有婚介这样一个相似容器的机构来控制。
在Spring中,IoC的思想也体如今对象建立和管理的流程上。全部的类都会在spring容器中登记,告诉spring你是个什么东西,你须要什么东西,而后spring会在系统运行到适当的时候,把你要的东西主动给你,同时也把你交给其余须要你的东西。全部的类的建立、销毁都由 spring来控制,也就是说控制对象生存周期的再也不是引用它的对象,而是spring。
在 Spring 中把每个须要管理的对象称为 Spring Bean(简称 Bean),而 Spring 管理这些 Bean 的容器,被咱们称为 Spring IoC 容器(或者简称 IoC 容器)。IoC 容器须要具有两个基本的功能:
在Spring中,全部的IoC容器都会实现BeanFactory这个接口。
public interface BeanFactory { String FACTORY_BEAN_PREFIX = "&"; //getBean 表示从容器获取对象的方法, // 能够经过对象的名字获取,也能够经过对象的类型获取。 Object getBean(String name) throws BeansException; <T> T getBean(String name, Class<T> var2) throws BeansException; Object getBean(String name, Object... var2) throws BeansException; <T> T getBean(Class<T> name) throws BeansException; <T> T getBean(Class<T> name, Object... var2) throws BeansException; // <T> ObjectProvider<T> getBeanProvider(Class<T> var1); <T> ObjectProvider<T> getBeanProvider(ResolvableType var1); //检测是否包含bean boolean containsBean(String name); //检测类是不是单例 默认状况下,全部的类型都是单例 boolean isSingleton(String name) throws NoSuchBeanDefinitionException; //返回true时,每次getBean都会建立一个新的对象。 boolean isPrototype(String name) throws NoSuchBeanDefinitionException; //检测bean是否匹配某个类型 boolean isTypeMatch(String name, ResolvableType var2) throws NoSuchBeanDefinitionException; boolean isTypeMatch(String name, Class<?> var2) throws NoSuchBeanDefinitionException; //获取一个bean的类型 @Nullable Class<?> getType(String name) throws NoSuchBeanDefinitionException; //获取一个bean的别名 String[] getAliases(String name); }
Bean属于最基础的IoC容器,只提供了对于注入对象最基本的操做,Spring在BeanFactory基础上还派生出了更高级的接口ApplicationContext,在实际使用中,大部分的Ioc容器都是ApplicationContext接口的实现类。
在BeanFactory基础上,ApplicationContext接口扩展了消息国际化接口,环境可配置接口,应用事件发布接口和资源模式解析接口。
在当前的Spring版本中,咱们多会使用注解的方式来装配Spring IoC容器(还有经过xml的方式),因此这里咱们就使用一个基于注解的IoC容器——AnnotationConfigApplicationContext。
咱们建立一个model包用于存放各类bean,而后建立一个bean 好比 User
public class User { private Long id; private String userName; private String note; /**setter and getter **/ .... }
而后再建立一个另外的包,好比config,而后在config中建立一个AppConfig文件
import com.guardz.first_spring_boot.model.User; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AppConfig { @Bean(name = "user") public User initUser() { User user = new User(); user.setId(1L); user.setUserName("user_name_1"); user.setNote("note_1"); return user; } }
关键在于注解,@Configuration表示这是一个Java配置文件,Spring会根据它来初始化IoC容器,用于装配Bean.
@Bean表示将initUser返回的对象放入IoC容器中进行管理,而这个对象的名字就是name属性定义的名字,若是没有定义name属性,默认就会使用方法名,好比 "initUser"。
而后,咱们就可使用AnnotationConfigApplicationContext 来建立一个IoC容器。
咱们修改XXXApplication文件,在main方法中加入以下代码
private static Logger log = Logger.getLogger(FirstSpringBootApplication.class.getName()); public static void main(String[] args) { SpringApplication.run(FirstSpringBootApplication.class, args); ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class); User user = applicationContext.getBean(User.class); log.info("this is yxwang " + user.getId()+""); }
运行后,咱们会看到打印出了
INFO 97781 --- [ main] c.g.f.FirstSpringBootApplication : this is yxwang 1
这就表示user对象已经正确装载到了applicationContext这个IoC容器中,而且咱们经过getBean,顺利拿到了这个user对象。
PS:这里有个疑惑,我在AppConfig中文件中移除了@Configuration的注解,发现也能正确注入,这让我很疑惑@Configuration的做用。不过姑且仍是加上吧,随着深刻学习,我想我会弄清楚的。
当咱们拥有很是多的bean以后,每个bean都写入AppConfig中去注入也是一种略显麻烦事情,所以,Spring还提供了一种扫描注入的方式,让IoC容器本身去扫描须要装配的bean。
首先须要改造一个bean
@Component("user") public class User { @Value("1") private Long id; @Value("user_name_1") private String userName; @Value("note_1") private String note; ... }
添加了@Component注解,这个注解表示,这个类须要被Spring IoC容器扫描装载。
@Value表示被注入时属性的值。
而后,咱们须要修改AppConfig的内容
@Configuration @ComponentScan public class AppConfig { }
删除了initUser的方法,添加了@ComponentScan的注解,这个注解表示当使用AppConfig初始化IoC容器时,须要主动扫描(扫描哪些@Component)。
运行,可是咱们发现报错了,没法找到User对象了,User没有被装配到IoC容器中。缘由在于@ComponentScan只会扫描当前包和它的子包。咱们的AppConfig和User并不在同一个包级下,因此没法被扫描到。
一种解决方案是修改包的结构,可是这未免会破坏原有的代码结构。好在@ComponentScan提供了很多参数
public @interface ComponentScan { //定义扫描的包 @AliasFor("basePackages") String[] value() default {}; //定义扫描的包 和 value互为别名 @AliasFor 表示和xx是别名 @AliasFor("value") String[] basePackages() default {}; //主动指出须要扫描的类名 Class<?>[] basePackageClasses() default {}; //Bean name的生成器 Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class; //做用于解析器 Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class; //做用于代理模式 ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT; //资源匹配模式 String resourcePattern() default ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN; //是否启用默认过滤器 boolean useDefaultFilters() default true; //过滤器,当知足条件时才会扫描载入 Filter[] includeFilters() default {}; //过滤器, 当知足条件时不会扫描载入 Filter[] excludeFilters() default {}; //是否延迟初始化 (对象被get时才会初始化bean) boolean lazyInit() default false; //过滤器 @Retention(RetentionPolicy.RUNTIME) @Target({}) @interface Filter { //过滤器类型,有不少种,默认是根据注解过滤 FilterType type() default FilterType.ANNOTATION; //定义过滤的类 @AliasFor("classes") Class<?>[] value() default {}; //定义过滤的类 @AliasFor("value") Class<?>[] classes() default {}; //匹配方式 String[] pattern() default {}; } }
比较经常使用的就是制定包名和过滤器。
如今,咱们能够修改AppConfig,修改成
//三种选一种 @ComponentScan("com.guardz.first_spring_boot.model") //@ComponentScan(basePackages = {"com.guardz.first_spring_boot.model"}) //@ComponentScan(basePackageClasses = {User.class})
假设我如今有一个@Service类,(@Service类内部会带有@Component注解)
@Service public class XXXService { .... }
这个时候AppConfig就会自动注入该类(假设该类的路径包含在了AppConfig设置的扫描路径中),这个时候,若是我不想注入该类,就可使用Filter进行排除,好比这样
@ComponentScan(basePackages = "xxxxx", excludeFilters = {@Filter(classes = {XXXService.class})})
PS: @AliasFor属性的互为别名必须在两个属性上都写上@AliasFor。另外AliasFor还能够用于覆盖指定注解中的值。
好比
.... public @interface EnableAutoConfiguration { ..... Class<?>[] exclude() default {}; } ... @EnableAutoConfiguration ... public @interface SpringBootApplication { /** * Exclude specific auto-configuration classes such that they will never be applied. * @return the classes to exclude */ //表示重写了EnableAutoConfiguration中的exclude值 @AliasFor(annotation = EnableAutoConfiguration.class) Class<?>[] exclude() default {}; ..... }
虽然第二种方式看上去很好用,可是对于三方类库,咱们没办法在对方的类中添加@Component注解,这个时候,仍是会使用@Bean标签。
引用咱们最上面的UserInfo和User类的例子,如今存在两个接口
public interface User{ public static Logger log = Logger.getLogger(User.class.getName()); public void setUserInfo(UserInfo userInfo); public void printUserInfo(); } public interface UserInfo { public String getUserType(); }
分别存在两个子类
@Component("clientInfo") public class ClientInfo implements UserInfo { @Override public String getUserType() { return "client"; } } @Component("gameUser") public class GameUser implements User { private UserInfo userInfo = null; @Override public void setUserInfo(UserInfo userInfo) { log.info("setUserInfo is called"); this.userInfo = userInfo; } @Override public void printUserInfo() { log.info("I am Game User ,type is "+userInfo.getUserType()); } }
这两个类都会被自动注入到IoC容器中,而后我在XXXApplication中使用
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class); User user = applicationContext.getBean(GameUser.class); user.setUserInfo(applicationContext.getBean(ClientInfo.class)); user.printUserInfo();
这操做并无问题,可是咱们发现咱们仍是参与了user的构造,调用了set方法,设置了一个值。而Spring的IoC容器正是致力于处理此类问题的工具。
咱们能够修改GameUser类
@Component("gameUser") public class GameUser implements User { @Autowired private UserInfo userInfo = null; ........ }
在userInfo关键字上添加了@Autowired关键字,表示让spring 根据类型自动注入对象。
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class); User user = applicationContext.getBean(GameUser.class); user.printUserInfo();
好了咱们能够直接如上这样获取对象,并调用方法了,UserInfo自动被附上了ClientInfo的值。
PS: 这里的赋值应该是直接经过反射写入的,由于setUserInfo方法并无被调用。(android中有些依赖注入框架会根据属性寻找相关的set方法,而后调用set方法)
那么问题来了,若是这是时候咱们新添加了一个类
@Component("adminInfo") public class AdminInfo implements UserInfo{ @Override public String getUserType() { return "admin"; } }
这个时候再去编译就会报错(IDEA都不须要编译,直接下划线告诉你有问题),为何,由于Spring 懵逼了,我有两个UserInfo的子类,你让我注入哪一个?
因此这个时候咱们能够将UserInfo类型改成ClientInfo,这样就能注入,可是这和设计思想不符啊,怎么就把我类型写死了?因此咱们就须要手动指出咱们须要的对象名字
@Component("gameUser") public class GameUser implements User { @Autowired @Qualifier("adminInfo") private UserInfo userInfo = null; ........ }
好比这里,咱们经过@Qualifier指定了注入的对象是"adminInfo"(这是AdminInfo在IoC容器中的名字,经过@Component指定了)。
除此以外还有几个使用方法也常常出现。
首先对于@Autowired注解,表示必定要找到一个惟一类进行注入,可是咱们能够经过 @Autowired(required = false) 表示要么找到惟一的类,要么找不到为空,不进行注入。
对于上述出现注入分歧的状况,咱们能够在ClientInfo中添加注解 @Primary 表示优先注入ClientInfo类型,这个时候能够把@Qualifier 注解删掉,Spring会选择ClientInfo进行注入。可是若是多个子类同时拥有@Primary 仍是会出现分歧报错。另外若是同时出现@Primary和@Qualifier,那么之后者为准。
@Autowired能够用在方法上,也可以实现自动调用
@Override @Autowired @Qualifier("adminInfo") public void setUserInfo(UserInfo userInfo) { log.info("setUserInfo is called"); this.userInfo = userInfo; }
最后,不少状况下构造函数式带有参数的,这个时候简单的添加@Component是没法进行注入的,这个时候@Autowired还能够用在参数上,好比这样
public GameUser(@Autowired @Qualifier("adminInfo") UserInfo userInfo){ this.userInfo = userInfo; }