前言
上节我们讲到了Spring的控制反转IoC,那么这一节的内容依然是建立在控制反转LoC的基础上做一些巩固和延伸啦。我们来回顾一下上节的IoC,相信大家应该知道什么是IoC了,它的意思就是控制反转,把对象的创建交给spring进行创建,那么我们经常会发现IoC和另外一个名词DI(Dependency Injection)搭配在一起使用,那么这两者的联系和区别到底在哪里呢?
IoC和DI的关系
那IoC之前我们已经定义过,我们再来看看DI的解释
DI—Dependency
Injection,即“依赖注入”:组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
看了关于DI的解释,我们发现出现频率最高的词是"依赖",那么依赖的是什么呢?为什么需要依赖?谁依赖于谁?谁注入于谁? 注入什么东西? 为何要注入?
在解答这个问题之前我们来举个小栗子~
本杰明从美国纽约来到武汉工作,小伙子想去租房子,但是人生地不熟的,找了快一个星期,终于找到一个价格适宜的房子,然后拎包入住了。恰巧隔壁的邻居是来自美国迈阿密的杰西卡,两个老美老乡见老乡,两眼泪汪汪啊!然后杰西卡就聊到了她的房子是在中介咨询中介帮忙找的,她就提出了她的要求,必须要在西北湖新华路附近,房间一定要干净整洁,直接拎包入住的那种,一天就搞定啦~
很简单的一个租房子的栗子,那么本杰明租的房子就好比我们普通的方式new出来的对象,杰西卡租到的房子就好比通过我们Spring框架的IoC获取到的对象,之前我们有提到过IoC里面的
ApplicationContext就是一个巨大的容器,在这里,中介就好比那个容器,有着各种各样的资源,客户只要发出请求,告诉他,我需要什么样的房子,那么这个请求,就好比id,通过这个id我们就可以在ApplicationContext中搜索相关的符合条件的"房子"(对象),那么中介通过客户需求找到房子,并按照房东的委托将匹配的房源租给租客,这就是DI依赖注入,客户找房子的角色切换成中介找房子这个角色,被称之为IoC控制反转
这么理解大家应该知道两者的关系和区别了吧~
好,那我们接着来解答上面的问题
依赖的是什么呢? 应用程序依赖于IoC/DI容器,依赖IoC/DI容器为它注入所需要的资源。
为什么需要依赖? 因为角色发生了反转,应用程序依赖的资源都是IoC/DI容器里面。
谁依赖于谁? 应用程序依赖于IoC/DI容器
谁注入于谁? IoC/DI容器注入于应用程序
注入什么东西? 注入应用程序需要的外部资源,比如依赖关系
为何要注入? 因为程序要正常运行需要这些外部资源
所以归根结底,从本质上来说IoC就是DI,只不过看问题的角度从"客户"切换成了"中介"而已。这两者是不可分割的存在,凡是程序里面需要使用外部资源的情况,都可以考虑使用IoC/DI容器,IoC/DI容器正诠释了设计理念之一 高内聚、低耦合
。
Spring的对象属性注入
上节的我们讲到了Spring创建对象的方式,也了解到了我们对象基本的一些属性注入的方式,比如说通过bean的中property子标签进行属性注入,这种注入方式,默认是调用set方法,还有就是我们的constructor-arg子标签的构造器注入,那么如果我们的属性中包含了诸如数组、List、Map,该如何进行属性注入呢?
那么我们还是通过栗子来看看怎么注入吧?(经常把例子打成栗子的我已经习惯用栗子了…)
这里纯粹为了示范,因此栗子不合常理大家也不要见怪啦~
老规矩,还是先建bean包(在Mybatis中通常叫做domain、Spring中叫pojo,都一样啦)
接下来,我们来写配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">
<!-- 通过构造器方式给对象user1赋值 -->
<bean id="user1" class="com.marco.domain.User">
<constructor-arg name="id" value="1"></constructor-arg>
<constructor-arg name="name" value="marco"></constructor-arg>
<constructor-arg name="age" value="18"></constructor-arg>
</bean>
<!-- 通过调用set方法给对象user2赋值 -->
<bean id="user2" class="com.marco.domain.User">
<constructor-arg name="id" value="1"></constructor-arg>
<constructor-arg name="name" value="sunnie"></constructor-arg>
<constructor-arg name="age" value="18"></constructor-arg>
</bean>
<!-- 通过调用set方法给对象person赋值 -->
<bean id="person" class="com.marco.domain.Person">
<property name="users">
<array>
<!-- 直接创建user对象 -->
<bean class="com.marco.domain.User">
<property name="id" value="1"></property>
<property name="name" value="marco"></property>
<property name="age" value="18"></property>
</bean>
<!-- 外部引用user对象 -->
<ref bean="user2"/>
</array>
</property>
<property name="userList">
<list>
<map value-type="com.marco.domain.User">
<!-- 外部引用user对象 -->
<entry key="marco zheng" value-ref="user1"></entry>
<entry key="sunnie zeng" value-ref="user2"></entry>
</map>
</list>
</property>
<property name="properties">
<props>
<prop key="UserDao">com.marco.dao.impl.UserDaoimpl</prop>
<prop key="UserService">com.marco.service.impl.UserServiceimpl</prop>
</props>
</property>
</bean>
</beans>
其实学习了Mybatis的基本配置,再来看Spring的基本配置,发现其实都差不多,只不过标签换了而已,因此大家也可以总结以下,一般碰到<property>标签就是属性,属性里name就是属性名,value就是属性值,class就是完全限定名,id在Spring的IoC中一般都是对象的名称,至于array、list、map标签,相信我不说,大家应该就懂了吧~
为了方便调用ApplicationContext对象,我这里做了简单的封装
public class ContextGenerator {
private static ApplicationContext context;
static {
context = new ClassPathXmlApplicationContext("applicationContext.xml");
}
public static ApplicationContext getContext() {
return context;
}
}
接下来我们测试一下,通过上述方式,是否能够实现JavaBean对象的注入
public class Test {
public static void main(String[] args) {
ApplicationContext context = ContextGenerator.getContext();
Person person = (Person) context.getBean("person");
System.out.println(person);
}
}
结果实在是太长啦,截取不了,我代大家看了结果,没有问题,请放心哦~
重构javaEE三层结构
一不小心,又讲了这么多关于IoC的内容,终于到了本的重点啦 【重构javaEE三层结构】
相信大家对MVC模型已经不陌生了吧,毕竟我已经提过挺多次了,你们不嫌烦,我自己都嫌烦啦,哈哈
每次学习了新的东西,反观之前学习的东西,总能发现问题或者说"落后的地方"。如果能发现问题,证明我们都在成长,这是一件好事,那大家可以思考一下,我们之前写的MVC的格局有什么地方可以改进下呢?(这里不谈我代码的逻辑问题哈)。
硬着头皮想,可能不一定想得出来,我就不打哑谜啦。我就把关键的部分给列举出来
大家有没有发现,我们之前在一个类中调用另外一个类的对象的方法的时候,都是我们自己直接new对象的,那么有没有办法不用我们自己new对象,或者说是有没有一套统一的方法,让我传什么"口令"就拿到什么对象呢?
结合我们之前学过的反射和IoC思想,我们试着完成一下,这里的dao包和service包我就省略掉了,直接上dao.impl和service.impl的内容,各位应该都能明白啦。
BeanFactory+Properties获取对象
为了简化代码突出重心,我这里就省略JDBC操作,模拟一下获取数据库值的操作了。
public class UserDaoimpl implements UserDao{
@Override
public User getUserById(Integer id) {
switch (id) {
case 1:
return new User(1,"marco", 18);
case 2:
return new User(2,"jack", 20);
case 3:
return new User(3,"rose", 18);
default:
return null;
}
}
}
这里我先停一下,既然之前的对象都是我们new出来的,那么通过反射和配置文件,是不是能建一个Bean工厂呢?
public class BeanFactory {
static Properties prop = new Properties();
static {
try {
prop.load(BeanFactory.class.getClassLoader().getResourceAsStream("config.properties"));
} catch (IOException e) {
e.printStackTrace();
}
}
@SuppressWarnings("unchecked")
public static <T> T newInstance(String key) {
String className = prop.getProperty(key);
Class<?> clz;
try {
clz = Class.forName(className);
return (T) clz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
配置文件、Service和Servlet我们就这么写
UserDao=com.marco.dao.impl.UserDaoimpl
UserService=com.marco.service.impl.UserServiceImpl
public class UserServiceImpl implements UserService{
@Override
public User getUserById(Integer id) {
UserDao userDao = (UserDao)BeanFactory.newInstance("UserDao");
return userDao.getUserById(id);
}
}
public class UserServlet {
public User getUserById(Integer id) {
UserService userService = (UserService)BeanFactory.newInstance("UserService");
User user = null;
if(null != id) {
user = userService.getUserById(id);
}
return user;
}
}
public class Test {
public static void main(String[] args) {
UserServlet userServlet = new UserServlet();
User user = userServlet.getUserById(1);
System.out.println(user);
}
}
测试结果如下,没有问题,通过这种方式,我们发现类与类之间解除了耦合关系,为什么这么说呢?
大家想象,通过之前的方式,如果我们改变了之中一个类的名字,那么类中引用的这个类的名字是不是全得换,现在我们得类还很少体会不到,如果有100个类都有当前类得引用呢?虽然很夸张,但是如果真的这样改起代码可是相当痛苦得一件事情,那通过现在这种方式,我们只用改配置文件中得类得完全限定名,其他得代码部分就根本不用修改了。
Spring+XML获取对象
方式一:通过beanId获取对象
那么既然通过properties文件结合反射可以获取对象,那么使用Spring+XML获取对象也就不在话下啦
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">
<bean id="userDao" class="com.marco.dao.impl.UserDaoimpl"></bean>
<bean id="userService" class="com.marco.service.impl.UserServiceImpl"></bean>
</beans>
我们只需要将之前获取UserDao对象和UserService对象得方法改成下面这种形式就可以啦
ApplicationContext context = ContextGenerator.getContext();
UserDao userDao = (UserDao)context.getBean("userDao")
ApplicationContext context = ContextGenerator.getContext();
UserService userService = (UserService)context.getBean("userService");
方式二:property注入
是不是很简单?如果你以为这样就结束就大错特错啦
我们把Service和Servlet修改一下
public class UserServiceImpl implements UserService{
UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public User getUserById(Integer id) {
return userDao.getUserById(id);
}
}
public class UserServlet {
UserService userService;
public void setUserService(UserService userService) {
this.userService = userService;
}
public User getUserById(Integer id) {
User user = null;
if(null != id) {
user = userService.getUserById(id);
}
return user;
}
}
写成这样是要闹哪样?不急接着看
public class Test {
public static void main(String[] args) {
ApplicationContext context = ContextGenerator.getContext();
UserServlet userServlet = context.getBean(UserServlet.class);
User user = userServlet.getUserById(1);
System.out.println(user);
}
}
很神奇对不?其实明白原理,发现也就那么回事儿,大家也看到了我用红线标注得地方,它实现得原理实际上就是使用我提到的DI依赖注入,之前我们得写法是将依赖注入关系体现在Java代码里面,而现在这种方式将这种依赖得关系体现在了我们得XML文件中,这样我们修改关系也显得更加灵活多变。
方式三:autowire自动装配注入
还没结束哦,我们接着上面得玩,好戏还在后面!
我们将userService和userServlet的bean标签的属性加上autowire=“byType”,autowire翻译过来就是自动组装,
autowire除了default还有byType和byName两个值,从字面上理解就是根据类型
和根据名称
,那么根据这两个来做什么呢?
<bean id="userDao" class="com.marco.dao.impl.UserDaoimpl" ></bean>
<bean id="userService" class="com.marco.service.impl.UserServiceImpl" autowire="byType">
</bean>
<bean id="userServlet" class="com.marco.servlet.UserServlet" autowire="byType">
</bean>
还是使用之前的测试代码不变,我们发现结果和之前的一模一样,其实autowire自动装配的就是我们的set方法,byType的意思就是通过set方法里面的要装配值得类型(比如说在UserServlet中得装备对象类型UserService),byName得意思就是装配值得名称(比如说在UserServlet中得装备对象名称userService),我们来做个测试
<bean id="userDao" class="com.marco.dao.impl.UserDaoimpl" ></bean>
<bean id="userService" class="com.marco.service.impl.UserServiceImpl" autowire="byType">
</bean>
<bean id="userService1" class="com.marco.service.impl.UserServiceImpl" autowire="byType">
</bean>
<bean id="userServlet" class="com.marco.servlet.UserServlet" autowire="byType">
</bean>
我们发现运行是有问题得,因为当userServlet根据Type来找UserServiceImpl的时候发现找到了两个一模一样的双胞胎对象,userServlet就懵了,到底谁是谁呢?这就是所谓的代码中的"真假美猴王"。
当我们把userServlet的autowire改成byName,程序又能正常运转,相信其中的道理大家应该都能明白了吧!
方式四:注解注入
玩累了,来介绍最后一种方法,使用注解注入!使用这种方式真的算的上是释放双手了,我们修改下之前的代码,将之前的set方法全部去掉。
@Component
public class UserDaoimpl implements UserDao{
@Override
public User getUserById(Integer id) {
switch (id) {
case 1:
return new User(1,"marco", 18);
case 2:
return new User(2,"jack", 20);
case 3:
return new User(3,"rose", 18);
default:
return null;
}
}
}
@Component
public class UserServiceImpl implements UserService{
@Autowired
UserDao userDao;
@Override
public User getUserById(Integer id) {
return userDao.getUserById(id);
}
}
@Component
public class UserServlet {
@Autowired
UserService userService;
public User getUserById(Integer id) {
User user = null;
if(null != id) {
user = userService.getUserById(id);
}
return user;
}
}
大家应该发现了,我在所有的类上加上注解@Component,可以把这个注解当作是将这些类放入到Spring的容器中, @Autowired就是自动装配的意思了,然后我们将xml结构分成以下4份
application-dao,application-service和application-servelt三分天下,将之前的属于自己type的标签归为己有,
三分天下之时,依然还是需要有人一统天下,否则添加大乱,民(程序猿)不聊生!
在applicationContext.xml中只用写上一句
<context:component-scan base-package="com.marco"></context:component-scan>
还是用之前的代码测试
需要注意,我们使用context:component-scan必须开启context,怎么开启呢?
点击xml文件下方的namespace中有个context就可以了,是不是很贴心!讲的这么详细都被我自己给的感动了…
讲了这么久还没有说component-scan是什么意思呢。component-scan实质上就是扫描的意思,他里面的属性base-package="com.marco"
就表示在哪个范围里扫描,我们之前不是给每一个关联的类加上了@Component标签么?说白了只有加上这个标签,我们才能扫描的到啦~
注解附录:
@Component 代表一个spring的组件
@Controller 被Component 注解了。主要作用于接收用户请求的类
@Service 被Component 注解了 主要作用于服务层
@Repository 被Component 注解了 主要作用于数据访问层
@Autowired 作用于类里面的属性或方法 ,用于自动从IOC容器里面根据类型去装配
接下来给大家补充一个小知识点啦,非常简单
关于外部配置引入的问题
上面我们接触了<context:component-scan></context:component-scan>
这个标签,不难知道我们xml文件是可以引入外部资源的,context这个属性正是为外部资源导入赋能。
之前我们不是使用properties来导入JDBC的配置信息么,那么我们尝试再结合XML来完成一下这个操作
<context:property-placeholder location="classpath:jdbc.properties" system-properties-mode="FALLBACK" />
<bean id="jdbc" class="com.marco.domain.JDBCConifg">
<property name="driverClass" value="${driverClass}"></property>
<property name="url" value="${url}"></property>
<property name="username" value="${username}"></property>
<property name="password" value="${password}"></property>
</bean>
context:property-placeholder
中的location就是我们properties文件的名称,注意了properties要在src根目录下,那么为什么要配置system-properties-mode="FALLBACK"
呢?
因为当我们使用${xxx}查找属性value的时候,会从系统路径开始扫描,而系统路径中可能也会有叫username的,比如说你的笔记本的用户名,那么为了避免这种事情发生,我们会把username改成别的名字比如说userName,或者加上这句话就可以啦,测试结果如下