spring 源码分析 -- 1 -- core 核心简介
sschrodinger
2019/03/04
Spring 框架简介
spring 框架是 Java 开发中最著名的轻量级框架,使用各种方式简化 Java 程序的开发。spring 采取了如下4个关键策略来简化程序的开发。
- 基于POJO的轻量级和最小侵入性编程
- 通过依赖注入和面向接口实现松耦合
- 基于切面和惯例进行声明式编程
- 通过切面和模板减少样板式代码
基于POJO的轻量级和最小侵入性编程
想象在没有 spring 框架时,我们构建 web 程序,需要显式的继承 HttpServlet 并且改写 doGet 等函数。样例代码如下:
class MyServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
PrintWriter writer = resp.getWriter();
writer.writer("Hello,world");
writer.flush();
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doGet(req, resp);
}
}
这样的程序和 servlet 协议紧耦合,必须要实现 HttpServlet 接口才能够编写程序。基于POJO的轻量级和最小侵入性编程的基本思想就是使用简单的 Java 语言( Plain Old Java Object),实现功能的实现。比如说,我们使用 spring 框架搭建 servlet,就可以写成如下形式:
@Controller
@RequestMapping("/")
public class MyController {
@RequestMapping(method = RequestMethod.GET)
public String hello() {
return "hello";
}
}
note
- POJO stands for Plain Old Java Object. It is an ordinary Java object, not bound by any special restriction other than those forced by the Java Language Specification and not requiring any class path.
- Extend prespecified classes, Ex: public class GFG extends javax.servlet.http.HttpServlet { … } is not a POJO class.
- Implement prespecified interfaces, Ex: public class Bar implements javax.ejb.EntityBean { … } is not a POJO class.
- Contain prespecified annotations, Ex: @javax.persistence.Entity public class Baz { … } is not a POJO class.
通过依赖注入和面向接口实现松耦合
IoC控制反转的实现方式是依赖注入。正常的 Java 程序中,我们在代码中使用新建对象的方式获得对象实例,但是在一些情况下,我们不知道使用具体的哪个类新建获得实例对象,比如说对数据库的访问。这个时候我们就需要定义接口,并在运行时自动选择具体的实现。IoC 将类的新建托付给第三方,并让第三方在运行时自动选择需要创建的具体类实现(反射机制)。
基于切面和惯例进行声明式编程
面向切面编程,即 AOP,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
想象在一个大型教育系统中,我们提供讲师服务,学生服务等服务,为了方便管理,我们需要为每一个服务添加日志接口,但是日志接口并不是各种服务的主要功能,为了松耦合,我们需要将日志服务单独出来。
在紧耦合的代码中,可能代码的结构如下:
public class StudentService {
public Logger logger;
service() {
try {
logger.logBefore();
//do some service
logger.logAfter();
} catch(Exception e) {
logError();
}
}
}
public class Logger {
public void logError() {
//log error
}
public void logBefore() {
//log before service
}
public void logAfter() {
//log after
}
}
在如上所示的代码中,StudentService 的 service 实现,将 日志记录代码也写了进去。但是日志记录并不应该出现在 service 的实现中,而是应该将 service 作为一个切点,将日志记录的功能添加在这个切点中,通常的做法是使用 xml 等配置文件进行配置,如下所示:
public class StudentService {
service() {
//do some service
}
}
public class Logger {
public void logError() {
//log error
}
public void logBefore() {
//log before service
}
public void logAfter() {
//log after
}
}
<配置>
在 service 之前执行Logger.logBefore()
在 service 之后执行Logger.logAfter()
在 service 错误之后执行Logger.logError()
配置>
相当于框架对 service 函数进行了包装。
通过切面和模板减少样板式代码
一些程序的代码会多次使用,比如说数据库的登陆等代码,spring 提供这些代码的模板,可以让人更加专注于实现自己的逻辑代码。
Spring 核心技术
Spring IOC
在我们的日常开发中,创建对象的操作随处可见以至于对其十分熟悉的同时又感觉十分繁琐,每次需要对象都需要亲手将其new出来,甚至某些情况下由于坏编程习惯还会造成对象无法被回收,这是相当糟糕的。但更为严重的是,我们一直倡导的松耦合,少入侵原则,这种情况下变得一无是处。于是前辈们开始谋求改变这种编程陋习,考虑如何使用编码更加解耦合,由此而来的解决方案是面向接口的编程,于是便有了如下写法:
public class BookServiceImpl {
//class
private BookDaoImpl bookDaoImpl;
public void oldCode(){
//原来的做法
bookDaoImpl=new bookDaoImpl();
bookDaoImpl.getAllCategories();
}
}
//=================new====================
public class BookServiceImpl {
//interface
private BookDao bookDao;
public void newCode(){
//变为面向接口编程
bookDao=new bookDaoImpl();
bookDao.getAllCategories();
}
}
BookServiceImpl 类中由原来直接与 BookDaoImpl 打交互变为 BookDao,即使 BookDao 最终实现依然是 BookDaoImp,这样的做的好处是显而易见的,所有调用都通过接口bookDao 来完成,而接口的真正的实现者和最终的执行者就是 BookDaoImpl,当替换 bookDaoImpl 类,也只需修 改bookDao 指向新的实现类。
虽然上述的代码在很大程度上降低了代码的耦合度,但是代码依旧存在入侵性和一定程度的耦合性,比如在修改 bookDao 的实现类时,仍然需求修改 BookServiceImpl 的内部代码,当依赖的类多起来时,查找和修改的过程也会显得相当糟糕,因此我们仍需要寻找一种方式,它可以令开发者在无需触及 BookServiceImpl 内容代码的情况下实现修改 bookDao 的实现类,以便达到最低的耦合度和最少入侵的目的。实际上存在一种称为反射的编程技术可以协助解决上述问题,反射是一种根据给出的完整类名(字符串方式)来动态地生成对象,这种编程方式可以让对象在生成时才决定到底是哪一种对象,因此可以这样假设,在某个配置文件,该文件已写好 bookDaoImpl 类的完全限定名称,通过读取该文件而获取到 bookDao 的真正实现类完全限定名称,然后通过反射技术在运行时动态生成该类,最终赋值给 bookDao 接口,也就解决了刚才的存在问题,这里为简单演示,使用 properties 文件作为配置文件,className.properties 如下:
bookDao.name=com.zejian.spring.dao.BookDaoImpl
获取该配置文件信息动态为bookDao生成实现类:
public class BookServiceImpl implements BookService
{
//读取配置文件的工具类
PropertiesUtil propertiesUtil = new PropertiesUtil("conf/className.properties");
private BookDao bookDao;
public void DaymicObject() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
//获取完全限定名称
String className=propertiesUtil.get("bookDao.name");
//通过反射
Class c=Class.forName(className);
//动态生成实例对象
bookDao= (BookDao) c.newInstance();
}
}
的确如我们所愿生成了 bookDao 的实例,这样做的好处是在替换 bookDao 实现类的情况只需修改配置文件的内容而无需触及 BookServiceImpl 的内部代码,从而把代码修改的过程转到配置文件中,相当于 BookServiceImpl 及其内部的 bookDao 通过配置文件与 bookDao 的实现类进行关联,这样 BookServiceImpl 与 bookDao 的实现类间也就实现了解耦合,当然 BookServiceImpl 类中存在着 BookDao 对象是无法避免的,毕竟这是协同工作的基础,我们只能最大程度去解耦合。
了解了上述的问题再来理解IOC就显得简单多了。Spring IOC 也是一个 Java 对象,在某些特定的时间被创建后,可以进行对其他对象的控制,包括初始化、创建、销毁等。简单地理解,在上述过程中,我们通过配置文件配置了 BookDaoImpl 实现类的完全限定名称,然后利用反射在运行时为 BookDao 创建实际实现类,包括 BookServiceImpl 的创建,Spring 的 IOC 容器都会帮我们完成,而我们唯一要做的就是把需要创建的类和其他类依赖的类以配置文件的方式告诉IOC容器需要创建那些类和注入哪些类即可。Spring 通过这种控制反转(IoC)的设计模式促进了松耦合,这种方式使一个对象依赖其它对象时会通过被动的方式传送进来(如 BookServiceImpl 被创建时,其依赖的 BookDao 的实现类也会同时被注入 BookServiceImpl 中),而不是通过手动创建这些类。我们可以把IoC模式看做是工厂模式的升华,可以把IoC看作是一个大工厂,只不过这个大工厂里要生成的对象都是在配置文件(XML)中给出定义的,然后利用 Java 的反射技术,根据 XML 中给出的类名生成相应的对象。从某种程度上来说,IoC 相当于把在工厂方法里通过硬编码创建对象的代码,改变为由 XML 文件来定义,也就是把工厂和对象生成这两者独立分隔开来,目的就是提高灵活性和可维护性,更是达到最低的耦合度,因此我们要明白所谓为的 IOC 就将对象的创建权,交由 Spring 完成,从此解放手动创建对象的过程,同时让类与类间的关系到达最低耦合度。
快速入门案例
理解了 Spring IOC 模式(容器)后,我们来看一个简单入门实例。使用 Spring 的 IOC 功能,必须先引入 Spring 的核心依赖包(使用 Maven 作为构建工具):
org.springframework
spring-core
${spring.version}
org.springframework
spring-beans
${spring.version}
org.springframework
spring-context
${spring.version}
然后创建 Dao 层(AccountDao):
public interface AccountDao {
void addAccount();
}
实现类(AccountDaoImpl):
public class AccountDaoImpl implements AccountDao{
@Override
public void addAccount() {
System.out.println("addAccount....");
}
}
再创建 Service,AccountService
public interface AccountService {
void doSomething();
}
实现类:
public class AccountServiceImpl implements AccountService {
/**
* 需要注入的对象
*/
private AccountDao accountDao;
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
public void doSomething() {
System.out.println("AccountServiceImpl#doSomething......");
accountDao.addAccount();
}
}
上面我们创建了 Dao 层和 Service 层的接口类及其实现类,其中 Service 层的操作依赖于 Dao 层,下面通过 Spring 的 IOC 容器帮助我们创建并注入这些类。IOC 使用的是 XML 配置文件,代码如下:
从 xml 文件中,我们需要声明一个beans的顶级标签,同时需要引入核心命名空间,Spring 的功能在使用时都需要声明相对应的命名空间,上述的命名空间是最基本的。然后通过 bean 子标签声明那些需要IOC容器帮助我们创建的类,其中 name 是指明 IOC 创建后该对象的名称(当然也可以使用 id 替换 name,这个后面会讲到),class 则是告诉 IOC 这个类的完全限定名称,IOC 就会通过这组信息利用反射技术帮助我们创建对应的类对象,如下:
接着我们还看到如下声明,accountService 声明中多出了一个 property 的标签,这个标签指向了我们刚才创建的 accountDao 对象,它的作用是把 accountDao 对象传递给 accountService 实现类中的 accountDao 属性,该属性必须拥有 set 方法才能注入成功,我们把这种往类 accountService 对象中注入其他对象(accountDao)的操作称为依赖注入,这个后面会分析到,其中的 name 必须与 AccountService 实现类中变量名称相同,到此我们就完成对需要创建的对象声明。接着看看如何使用它们。
使用这些类需要利用 Spring 提供的核心类,ApplicationContext
,通过该类去加载已声明好的配置文件,然后便可以获取到我们需要的类了。
public void testByXml() throws Exception {
//加载配置文件
ApplicationContext applicationContext=new ClassPathXmlApplicationContext("spring/spring-ioc.xml");
// AccountService accountService=applicationContext.getBean("accountService",AccountService.class);
//多次获取并不会创建多个accountService对象,因为spring默认创建是单实例的作用域
AccountService accountService= (AccountService) applicationContext.getBean("accountService");
accountService.doSomething();
}
循环依赖
如下有两个 bean,A 和 B,这两个 bean 通过构造函数互为依赖,这种情况下 Spring 容器将无法实例化这两个 bean。
public class A{
private B b;
public A(B b){
this.b=b;
}
}
public class B{
private A a;
public B(A a){
this.a=a;
}
}
这是由于 A 被创建时,希望 B 被注入到自身,然而,此时 B 还有没有被创建,而且 B 也依赖于 A,这样将导致 Spring 容器左右为难,无法满足两方需求,最后脑袋奔溃,抛出异常。解决这种困境的方式是使用 Setter 依赖,但还是会造成一些不必要的困扰,因此,强烈不建议在配置文件中使用循环依赖。
AOP 编程
面向切面编程,使得代码能够更加专注于该业务的逻辑。
比如说日志系统,许多系统模块都会需要用到日志系统,但是每个模块如果都增加对日志的写入会显得逻辑代码不清晰,因为日志代码并不是该模块的主要功能,如下伪代码:
class Model {
public void addUser(User user) {
log.info("start...");
try {
add(user);
log.info("add user success");
} catch (IOException e) {
log.err("add user error");
}
}
Logger log = LoggerFactory.getInstance();
}
如上代码展示了一个典型的日志系统的写入功能,可以发现日志系统占了逻辑的一大块,使得本身的逻辑不明显。面向切面编程更加关注横切面,即真正的业务逻辑,将日志记录通过 class 文件修改或者动态代理等方式,实现日志记录的功能。