用了Spring这么久了,一直很想写一篇系列的文章来总结一下自己对Spring的理解。
在学习Spring之前,首先总结下以前的web中写的代码,当然这里可以不利用springmvc来做为基础框架,也可以自己来进行定义
下面按照MVC三层架构来写:
首先从controller层中入手:
public class HelloController {
private HelloService helloService;
public String hello(){
String result = helloService.hello();
return "hello";
}
}
再到service层:
public class HelloService {
private HelloDao helloDao;
public String hello() {
String result = helloDao.findById();
return result;
}
}
最终到dao
public class HelloDao {
public String findById() {
return "hello";
}
}
那么从上面不难看出,从controller层中使用到helloService对象,从service层中需要利用到helloDao对象。
在javaweb阶段的时候,这里采用的方式是直接new一个,其实这里也就是多例模式(接下来会讲),在使用完成之后,就立即释放,然后被JVM回收掉。
但是在这样的一种设计中,存在着一个最大的问题。那么就是controller强依赖于service
最常见的就是A类、B类、D类都依赖于C类,后期想要进行修改,比如说B类需要依赖于E类,那么就会对B类的源码进行修改,但是不能够轻易修改,因为可能会对依赖于B的类造成问题。
在这里放上一个链接,觉得讲解的不错:https://blog.csdn.net/qq_38157516/article/details/81979219
但是上面介绍的是初衷,理想化的状态。我们有时候又不得不去进行依赖,但是要求耦合性又没有原来那么高。
因为controller中强依赖于service,service又强依赖于dao。那么将会导致如果修改了service的代码,controller中也将会来进行修改,所以无法进行操作。
所以首先需要解决依赖问题。那么先自己来实现一个解耦合的案例:
service层:
public interface UserService {
void save();
}
public class UserServiceImpl implements UserService {
/**
* 存在编译期依赖:如果没有UserDaoImpl,代码编译是不通过的。
* 要避免编译期依赖,减少运行期依赖
* 解决思路:
* 1. 使用反射技术代替new
* 2. 提取配置文件
*/
//private UserDao userDao = new UserDaoImpl();
private UserDao userDao = (UserDao) BeanFactory.getBean("userDao");
@Override
public void save() {
userDao.save();
}
}
紧接着对BeanFactory来进行实现,这里才是核心!因为要在这里进行解耦实现
配置文件beans.properties
userDao=com.guang.dao.impl.UserDaoImpl
public class BeanFactory {
private static Map<String, Object> map = new HashMap<>();
static {
// 类加载时,读取properties文件,把其中所有的bean都创建对象,放到一个容器里
// 当需要使用时,直接从容器中获取即可
try {
//1.读取配置文件
ResourceBundle bundle = ResourceBundle.getBundle("beans");
Enumeration<String> keys = bundle.getKeys();
while (keys.hasMoreElements()) {
String id = keys.nextElement();
String className = bundle.getString(id);
Class clazz = Class.forName(className);
Object object = clazz.newInstance();
map.put(id, object);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 对外暴露获取得到方法
public static Object getBean(String id){
return map.get(id);
}
}
从上面的代码中可以看出来,这里的解耦并非是完全的解耦,但是降低了耦合度。不需要按照原来的方式:
private UserDao userDao = new UserDaoImpl();
而是通过
private UserDao userDao = (UserDao) BeanFactory.getBean("userDao");
这样子来获取,那么即使UserDao中的代码发生了改变,我只需要修改service中的一部分代码或者是再添加个接口即可。
首先需要了解一下spring中的基本的概念。
IOC是Inversion of Control的缩写,多数书籍翻译成“控制反转”。控制反转的意义就在于将创建对象的控制权让出去,而不是交给程序员来创建,交给了spring框架去创建。
DI是Dependency Injection的缩写,也被翻译成是依赖注入。我觉得DI可以是IOC的一部分,最粗俗的理解就是DI是将控制反转后的对象让其他对象去依赖,但是控制反转的对象又不一定要被其他组件依赖。
最简单的案例就是:service要被controller来依赖,但是controller可能不需要被其他的对象依赖。所以我觉得DI是IOC的一部分,但是网上很多资料都会把二者当前是一样的来处理。
在spring中,习惯上将容器中的对象叫做组件
那么既然介绍了这两种,赶紧趁着手热来实现一下:
使用环境:jdk8+maven+IDEA
第一步:引入依赖:
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.9</version>
</dependency>
第二步:controller和service
controller层:
public class HelloController {
private HelloService helloService;
public void setHelloService(HelloService helloService) {
this.helloService = helloService;
}
public String hello(){
helloService.hello();
return "hello";
}
}
service层:
public interface HelloService {
String hello();
}
service实现层:
public class HelloServiceImpl implements HelloService {
@Override
public String hello() {
return "hello";
}
}
配置文件:bean.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="service.impl.HelloServiceImpl" id="helloService"/>
<bean class="controller.HelloController" id="helloController">
<property name="helloService" ref="helloService"/>
</bean>
</beans>
然后编写一个测试类来进行测试:
public class BeanFactoryTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:beans.xml");
HelloController helloController = (HelloController) applicationContext.getBean("helloController");
String hello = helloController.hello();
System.out.println("hello");
}
}
控制台输出一下:
hello
那么接下来结合着代码和配置文件解释下里面的内容:
public class HelloController {
// controller中依赖于HelloService,那么这个属性在使用的时候spring容器中应该要存在,所以要对将这个对象添加到容器中去
// 但是这个是接口,无法创建对象,那么就只能够找实现类了
private HelloService helloService;
public void setHelloService(HelloService helloService) {
this.helloService = helloService;
}
public String hello(){
helloService.hello();
return "hello";
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="service.impl.HelloServiceImpl" id="helloService"/>
<!--这里是将HelloController注册到容器中去,id是唯一的,通过这个id就可以从容器中获取得到HelloController对象-->
<bean class="controller.HelloController" id="helloController">
<!--这里的property属性,表示的就是HelloController中的属性,ref表示的是引用类型的,引用的是上面注册到容器中的对象(组件)-->
<!--其实这里就是DI,依赖注入,因为HelloController中依赖了helloService,将上面的HelloService注入到HelloController的属性中去-->
<!--既然有引用,那么对应的就有普通类型的value-->
<property name="helloService" ref="helloService"/>
</bean>
</beans>
bean组件首先要实例化后才能够放入到容器中,spring提供了几种bean实例化的方式
工厂非静态方法、工厂静态方法、无参构造方法(最常用)
工厂静态方法实例化:
public class StaticMethodFactory{
public static UserDao createUserDao(){
return new UserDaoImpl();
}
}
配置文件:
<bean id="userDao" class="com.guang.bean.StaticMethodFactory" factory-method="createUserDao"></bean>
配置文件中指明了使用StaticMethodFactory类中的createUserDao方法来进行实例化bean;
工厂非静态方法实例化:
public class NoStaticMethodFactory{
public UserDao createUserDao(){
return new UserDaoImpl();
}
}
配置文件:
<!-- 先配置工厂 -->
<bean id="noStaticMethodFactory" class="com.guang.factory.NoStaticMethodFactory"></bean>
<!-- 再配置UserDao -->
<!-- factory-bean是工厂bean的名字,使用的是工厂bean中的非静态方法createUserDao来创建出来的userDao组件 -->
<bean id="userDao" factory-bean="instanceFactory" factory-method="createUserDao"></bean>
最常见的其实通过set方式注入,还有其他的,比如说构造方法注入、P标签,但是使用构造方法和P标签注入太过于麻烦,直接使用set方式进行注入即可。
在上面的案例中:
public class HelloController {
private HelloService helloService;
// 给属性提供了set方法,所以这里当然也是可以通过构造方法来进行注入
public void setHelloService(HelloService helloService) {
this.helloService = helloService;
}
public String hello(){
helloService.hello();
return "hello";
}
}
但是spring的注解版的参数中并没有提供set方法,那么是通过暴力反射的方式来给参数注入;
注解版的在spring中可能并不是太过于明显,但是在springboot项目中,可以使用纯注解。
在spring中通常使用的是注解+配置文件的方式来使用。注解版的使用方便,但是也需要注意其使用原理。
常用注解:
注解 | 说明 |
---|---|
@Component |
用在类上,相当于bean标签 |
@Controller |
用在web层类上,配置一个bean(是@Component 的衍生注解) |
@Service |
用在service层类上,配置一个bean(是@Component 的衍生注解) |
@Repository |
用在dao层类上,配置一个bean(是@Component 的衍生注解) |
service层:
public interface UserService {
void save();
}
@Service("userService")//等价于@Conponent("userService")
public class UserServiceImpl implements UserService {
//注意:使用注解配置依赖注入时,不需要再有set方法
@Autowired
//@Qualifier("userDao")
private UserDao userDao;
@Override
public void save() {
userDao.save();
}
}
dao层:
public interface UserDao {
void save();
}
@Repository("userDao")//等价于@Component("userDao")
public class UserDaoImpl implements UserDao {
@Override
public void save() {
System.out.println("save.....method");
}
}
配置文件:
<?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.xsd">
<!--开启组件扫描,将这个包下的所有了加上了能够成为bean的注解的类都添加成容器中的组件-->
<context:component-scan base-package="com.guang"/>
</beans>
注解 | 说明 |
---|---|
@Autowired |
相当于property标签的ref |
@Qualifier |
结合@Autowired 使用,用于根据名称注入依赖 |
@Resource |
相当于@Autowired + @Qualifier ,JDK提供的 |
@Value |
相当于property标签的value |
如果按照类别(@Autowired)进行注入的话,最常见的就是controller中使用的是属性的数据类型是接口,那么这个时候应该注意,如果有多个类型的组件,那么
就需要使用到@Qualifier来知名使用哪种方式。
bean是通过实例化方式来进行创建的,实例化之后,需要进行初始化,最终还需要被销毁。
bean的作用范围在配置文件中是以scope属性来进行配置的。
取值 | 说明 |
---|---|
singleton |
默认,表示单例的,一个Spring容器里,只有一个该bean对象 |
prototype |
多例的,一个Spring容器里,有多个该bean对象 |
request |
web项目里,Spring创建的bean对象将放到request 域中:一次请求期间有效 |
session |
web项目里,Spring创建的bean对象将放到session 域中:一次会话期间有效 |
globalSession |
web项目里,应用在Portlet环境/集群环境 |
不同scope的bean,生命周期:
singleton:bean的生命周期和Spring容器的生命周期相同
prototype:bean的生命周期和Spring容器无关。Spring创建bean对象之后,交给JVM管理了
何时创建:调用getBean
方法获取bean对象时,bean对象创建
spring提供了两种方式来进行初始化:
init-method
:指定类中初始化方法名称,该方法将在bean对象被创建时执行destroy-method
:指定类中销毁方法名称,该方法将在bean对象被销毁时执行但是我们一般不会使用这种方式,在springboot我们会实现一个接口:
public interface InitializingBean {
void afterPropertiesSet() throws Exception;
}
在afterPropertiesSet中来对bean做初始化
比如:
@Component
public class User implements InitializingBean {
private Integer age;
private String username;
// 没有提供set/get方法
@Override
public void afterPropertiesSet() throws Exception {
this.age = 12;
this.username = "guang";
}
@Override
public String toString() {
return "User{" +
"age=" + age +
", username=‘" + username + ‘\‘‘ +
‘}‘;
}
}
测试类中:
public class BeanFactoryTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:beans.xml");
User user = applicationContext.getBean(User.class);
System.out.println(user.toString());
}
}
但是最终在控制台显示的是:
User{age=12, username=‘guang‘}
说明了实现了这个接口的,不需要来提供set方法给属性进行赋值,注解版的同样如此。
关于销毁方法,也提供了一个接口:
public interface DisposableBean {
void destroy() throws Exception;
}
案例:
@Component
@Scope(value = "prototype")
public class User implements InitializingBean, DisposableBean {
private Integer age;
private String username;
// 没有提供set/get方法
@Override
public void afterPropertiesSet() throws Exception {
this.age = 12;
this.username = "guang";
}
@Override
public String toString() {
return "User{" +
"age=" + age +
", username=‘" + username + ‘\‘‘ +
‘}‘;
}
@Override
public void destroy() throws Exception {
System.out.println("销毁对象之前需要调用的方法");
}
}
测试方法:
public class BeanFactoryTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:beans.xml");
User user = applicationContext.getBean(User.class);
System.out.println(user.toString());
System.out.println("IOC容器准备关闭");
applicationContext.close();
}
}
但是控制台显示:
User{age=12, username=‘guang‘}
IOC容器准备关闭
但是我们的销毁方法并没有执行!查了下资料,如果是多例的话,并不会自动调用,而是因为多例的对象不由IOC容器来进行管理。如果是单例的组件,那么就会来调用这个方法,那么测试一下:
@Component
// 默认就是单例
public class User implements InitializingBean, DisposableBean {
private Integer age;
private String username;
// 没有提供set/get方法
@Override
public void afterPropertiesSet() throws Exception {
this.age = 12;
this.username = "guang";
}
@Override
public String toString() {
return "User{" +
"age=" + age +
", username=‘" + username + ‘\‘‘ +
‘}‘;
}
@Override
public void destroy() throws Exception {
System.out.println("销毁对象之前需要调用的方法");
}
}
控制台输出:
User{age=12, username=‘guang‘}
IOC容器准备关闭
销毁对象之前需要调用的方法
前面在并发包专题也介绍了这个问题,问题是先找到多线程的入口在哪里。tomcat服务器在接收到client的请求之后,每个请求就是一个线程,所以在达到我们能够处理的地方的时候,就已经产生了多线程。比如说filter、controller中已经有了多线程。我们可以在filter中使用ThreadLocal等给线程绑定数据等等,常规操作。
所以,也就说明了在controller、service中面临着线程安全问题,所以尽量不要在类中成员变量中定义变量。
但是我们在controller、service中会注入springIOC容器中的对象,因为默认是单例的,所以尽量操作方法,而不是需要操作变量。因为对于多线程来说,每个方法都会在自己线程的栈空间中执行,而唯一能够产生线程安全问题的就是这些个数据。所以要注意这些数据的使用。
我们通常的使用方式就是在业务层service中定义一把锁:
private final ReentrantLock lock = new ReentrantLock();
我们使用其中的lock和unlock方法来做线程安全处理。保证在某一个时间段内,只有一个线程可以操作。
这种方式操作起来比较方便,当然也可以使用syncronized关键字,因为对象是单例的,所以也可以使用this关键字。但是通常来说都是利用了lock锁来解决这种问题。
可以利用这种方式来操作缓存等,比较方便!
从传统web方式出发到spring框架,最直观的感觉就是spring利用IOC来帮助我们解决了解耦问题,其实这也是最直观的方式。
对象的创建交给了Spring容器,我们在使用的时候需要通过DI来进行注入即可,但是创建组件是利用了对应的注解,需要扫描到之后添加到Spring容器中去。
那么又有了组件的生命周期和作用域问题以及带来的线程安全问题。
所以主线是:
组件所属类------->成为组件注解----------->生命周期-------->作用范围---------->DI---------->线程安全
原文:https://www.cnblogs.com/likeguang/p/15100468.html