diff --git "a/week_07/25/Spring\347\232\204Ioc\343\200\201DI\344\270\216Aop\346\246\202\345\277\265\346\200\273\347\273\223.md" "b/week_07/25/Spring\347\232\204Ioc\343\200\201DI\344\270\216Aop\346\246\202\345\277\265\346\200\273\347\273\223.md" new file mode 100644 index 0000000000000000000000000000000000000000..bc8b2511254edcbd3e52aa459bd19340a3df4eb0 --- /dev/null +++ "b/week_07/25/Spring\347\232\204Ioc\343\200\201DI\344\270\216Aop\346\246\202\345\277\265\346\200\273\347\273\223.md" @@ -0,0 +1,227 @@ +Spring的基础机制就是其家喻户晓的Aop和IoC了。其中IoC主要采用了工厂模式的思想,用一个或多个工厂(取决于ApplicationContext的实例个数)负责整个程序的对象创建与管理过程。而Aop则主要采用了代理模式的思想,动态地对类中的方法进行增强,实现系统业务逻辑与系统服务(例如日志、安全等)的分离。除此以外,Spring还提供了事务管理、对象关系映射、事件机制等核心模块。 + + + +### Spring-IoC + +提到IoC我们都会联想到Spring,但IoC并不是Spring独家的,很多框架都采用IoC的思想,只是我们在初学时第一次接触IoC都是通过Spring。IoC其实是一种 Inversion of Controller 原则,它要求将对象创建的控制权由我们自身转交给框架,以此达到解耦的目的。 + +下面通过一个代码示例解释控制反转: + +```java +public class UserServiceTest { + public static boolean doTest() { + // ... + } + + public static void main(String[] args) {//这部分逻辑可以放到框架中 + if (doTest()) { + System.out.println("Test succeed."); + } else { + System.out.println("Test failed."); + } + } +} +``` + +上面这段代码中,所有的测试流程都是由程序员控制,但如果将测试流程抽象为框架,那么控制权就由程序员转交给了框架,以此实现了控制反转的目的: + +```java +public abstract class TestCase { + public void run() { + //根据方法执行是否成功, 输出不同的结果 + if (doTest()) { + System.out.println("Test succeed."); + } else { + System.out.println("Test failed."); + } + } + + public abstract boolean doTest(); +} + + +public class JunitApplication { + //任务容器, 包含了测试任务的所有Bean + private static final List testCases = new ArrayList<>(); + + //添加测试样例到容器 + public static void register(TestCase testCase) { + testCases.add(testCase); + } + + //遍历容器, 执行测试逻辑 + public static final void main(String[] args) { + for (TestCase case: testCases) { + case.run(); + } + } +} + +//测试框架使用----------------------- + +//测试类继承抽象类 +public class UserServiceTest extends TestCase { + + //需要测试的方法逻辑 + @Override + public boolean doTest() { + // ... + } +} + +// 注册操作(还可以通过配置的方式来实现) +JunitApplication.register(new UserServiceTest()); +``` + +上述代码其实就是Junit测试框架的主要思想,将测试逻辑的执行交给框架处理而不是由程序员手动处理。由此可见,IoC的思想其实是普遍存在的。我们学习IoC不能仅仅局限于Spring的IoC逻辑,而是抽取其中最核心思想。 + +现在我们再回过头来看Spring的IoC,其实本质上就是将我们创建对象的逻辑转交给Spring框架处理。与Junit框架的区别仅仅在于控制反转的对象不同。 + + + +### Spring-DI + +同样的,DI也不是Spring独家的。依赖注入顾名思义就是不通过 new() 的方式在类内部创建依赖的对象,而是将依赖的对象在外部创建好之后,通过构造函数或其他方式注入给类使用。 + +在Spring中,我们可以通过容器实现依赖注入。但即便我们不使用Spring,也依然能够通过构造函数实现依赖注入: + +```java +//非依赖注入实现方式-------------------- +public class Notification { + private MessageSender messageSender; + + public Notification() { + this.messageSender = new MessageSender();//Notification内部创建massageSender + } +} + + +//依赖注入的实现方式------------------- +public class Notification { + private MessageSender messageSender; + + // 通过构造函数将messageSender注入, 这里的MessageSender来自Notification外部 + public Notification(MessageSender messageSender) { + this.messageSender = messageSender; + } +} +``` + +上面这段代码虽然实现了依赖注入,但是创建对象、以及组装对象的过程也仅仅是被移动到了上层代码。因此我们依然要在上层代码中进行手动实现: + +```java +public class Demo { + public static final void main(String args[]) { + MessageSender sender = new SmsSender(); //创建对象 + Notification notification = new Notification(sender);//依赖注入 + } +} +``` + +考虑到软件开发中,一个项目涉及到了成百上千个类,手动的创建对象以及依赖注入会变得非常复杂,因此才选择将这部分的工作抽象为框架自动完成。比如Spring的IoC容器在负责对象创建的同时,也会根据注解自动实现依赖注入的功能,可以说是非常方便了。 + + + +上述内容中,分析了IoC与DI这两种设计思想。如果今后面试中提到IoC与DI,千万不要局限在Spring层面上了,一定要结合设计思想来回答。 + + + +### Spring-Aop + +Spring-Aop是Spring提供的面向切面编程机制,它可以分离系统业务逻辑和系统服务(日志、安全),实现了业务与其他逻辑的解耦,提高了代码的复用性。 + +Aop的使用场景一般是对某个功能进行增强,比如:当你开发一个登陆功能时,你需要在用户登陆前后执行权限校验,并将校验信息写入到日志文件中。如果你将校验逻辑都混杂在登陆功能的逻辑中,不满足SOLID原则中的单一职责原则不说,倘若另一个模块的某个功能也需要在前后执行权限校验,那么还得将校验逻辑复制粘贴,这又降低了代码的复用性。 + +这时,我们可以考虑将业务逻辑无关的代码都抽取出来,并且在程序运行时,在需要这部分逻辑的位置(连接点)进行动态插入。而这种方式的实现,也就是Spring提供的面向切面编程。 + +SpringAop有如下几个概念: + +1. 通知:指定在某个时间点(比如方法被调用前、方法执行结束后等)对方法进行增强。通知有如下5种: + * Before:在方法调用前增强。 + * After:在方法执行完成后增强,无论是否成功返回。 + * After-returning:在方法成功执行且返回之后增强。 + * After-throwing:在方法抛出异常后增强。 + * Around:在方法调用前、后进行增强。 +2. 切入点:也就是所谓的原始类中需要被增强的方法。 +3. 连接点:比如方法调用前、方法返回后、抛出异常后。 +4. 切面:切入点和通知的集合,一般单独作为一个类,共同定义关于切面的全部内容。 +5. 织入:在程序运行时,动态增强方法的这个过程。 +6. 引用:允许我们向现有的类添加新的方法或属性。 + +Spring对Aop的实现同时支持了CGLIB,AspectJ,JDK动态代理,当需要增强的类实现了接口时,Spring会默认采用JDK动态代理,否则采用CGLIB。 + +下面我们通过一组代码示例具体看看Spring Aop的操作流程: + +首先是主题接口: + +```java +public interface User { + + void login(); + + void download(); +} +``` + +实现类: + +```java +public class UserImpl implements User { + + public void login() { + ... + } + + public void download() { + ... + } +} +``` + +假设我们需要对login()和download()增强,login()需要在登陆之前进行登陆校验工作;download()需要在执行前进行权限校验工作。 + +现在我们需要定义切面类,切面中包含了切点与通知: + +```java +public class PermissionVerification { + + public void verifyLogin() {//登陆校验 + ... + } + + public void saveMessage() {//权限校验 + ... + } + +} +``` + +定义SpringAop.xml文件: + +```xml + + + + + + + + + + + + + + + + +``` + diff --git "a/week_07/25/spring_ioc\346\211\247\350\241\214\346\265\201\347\250\213.md" "b/week_07/25/spring_ioc\346\211\247\350\241\214\346\265\201\347\250\213.md" new file mode 100644 index 0000000000000000000000000000000000000000..ec9f89ac0d8685ad17f5fdc282e4db285febea5e --- /dev/null +++ "b/week_07/25/spring_ioc\346\211\247\350\241\214\346\265\201\347\250\213.md" @@ -0,0 +1,286 @@ +### 工厂模式与DI框架 + +当创建对象是一个大工程时,通常我们可以采用工厂模式将创建对象的逻辑与使用的逻辑分离(单一职责原则),采用一个工厂类专门负责对象的创建工作。但工厂模式与DI框架的区别在于:工厂模式只负责某个类或某组相同性质的类实例创建,说白了工厂类需要创建哪些类都在代码中写死了;而DI框架则是将 “工厂模式创建对象的行为” 抽象成了框架,专门负责整个应用中的类对象创建。 + +至于DI框架是如何获取创建的实例信息,一般采用配置文件的方式来提供。下面我们就通过源码分析Spring框架创建bean与获取bean的执行流程。 + + + +### Spring-Ioc容器的执行流程 + +下面这段代码涉及到了Spring的bean实例创建与获取动作: + +```java +public static void main(String[] args) { + //ClassPath下寻找xml配置文件, 然后根据xml文件来构建ApplicationContext + ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml"); + UserService userService = ac.getBean(UserService.class); + userService.sayHello(); +} +``` + +我们省略配置文件以及跳过非重点代码,直接看ClassPathXMLApplicationContext的初始化流程。ClassPathXMLApplicationContext,顾名思义就是通过传入xml文件的classpath创建ApplicationContext,而ApplicationContext则负责创建Bean以及依赖注入的工作。 + +```JAVA +public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent) throws BeansException { + super(parent); + //根据提供的classpath处理乘配置文件数组 + this.setConfigLocations(configLocations); + //若refresh标识为true, 则执行refresh操作 + if (refresh) { + this.refresh(); + } +} +``` + +refresh() 方法会将之前的ApplicationContext销毁,然后重新建立一次初始化操作。并且该方法也是Spring的核心方法。也就是我们创建工厂、实例化Bean、以及依赖注入的工作都是在 refresh() 中完成的。 + +下面是 refresh() 的源码: + +```java +public void refresh() throws BeansException, IllegalStateException { + Object var1 = this.startupShutdownMonitor; + //加锁, 避免其他线程干扰启动或销毁容器的操作 + synchronized(this.startupShutdownMonitor) { + //执行准备工作, 例如记录容器启动时间、标识“已启动”状态等 + this.prepareRefresh(); + //核心步骤, 执行初始化BeanFactory、加载Bean、注册Bean的流程 + //注意: 这里还没有完成Bean的初始化流程 + ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory(); + //准备bean容器 + this.prepareBeanFactory(beanFactory); + + try { + this.postProcessBeanFactory(beanFactory); + this.invokeBeanFactoryPostProcessors(beanFactory); + //注册BeanPostProcessor实例 + this.registerBeanPostProcessors(beanFactory); + //初始化ApplicationContext的消息源 + this.initMessageSource(); + //初始化当前ApplicationContext的事件广播器 + this.initApplicationEventMulticaster(); + //初始化一些特殊的bean + this.onRefresh(); + //注册事件监听器 + this.registerListeners(); + //负责初始化所有的单例bean + this.finishBeanFactoryInitialization(beanFactory); + //结束refresh()操作, 事件广播ApplicationContext初始化完成 + this.finishRefresh(); + } catch (BeansException var9) {//若执行过程中抛出异常 + //若警报日志开启, 则写入日志 + if (this.logger.isWarnEnabled()) { + this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9); + } + //销毁已经初始化的单例Bean + this.destroyBeans(); + //重置refresh标志位 + this.cancelRefresh(var9); + throw var9; + } finally { + //重置缓存 + this.resetCommonCaches(); + } + + } +} +``` + + 上面的几个方法我们重点关注初始化BeanFactory的逻辑,也就是代码第十行,obtainFreshBeanFactory() : + +```java +protected ConfigurableListableBeanFactory obtainFreshBeanFactory() { + //关闭旧的 BeanFactory (如果有, 创建新的 BeanFactory, 加载 Bean 定义、注册 Bean 等等 + //核心都在这里了 + refreshBeanFactory(); + + // 返回刚刚创建的 BeanFactory + ConfigurableListableBeanFactory beanFactory = getBeanFactory(); + if (logger.isDebugEnabled()) { + logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory); + } + return beanFactory; +} +``` + +我们继续深挖 refreshBeanFactory() : + +```java +protected final void refreshBeanFactory() throws BeansException { + //若ApplicationContext中已经加载过beanFactory, 那么则销毁所有的Beans, 关闭这个BeanFactory + if (this.hasBeanFactory()) { + this.destroyBeans(); + this.closeBeanFactory(); + } + try { + //初始化一个DefaultListableBeanFactory类型的实例 + DefaultListableBeanFactory beanFactory = this.createBeanFactory(); + //设置序列化id + beanFactory.setSerializationId(this.getId()); + //客制化上面创建的beanFactory: 是否允许Bean覆盖、是否允许循环引用 + this.customizeBeanFactory(beanFactory); + //加载BeanDefinition到这个beanFactory中(BeanDefinition可以理解为Ioc工厂Bean的通用模版) + this.loadBeanDefinitions(beanFactory); + Object var2 = this.beanFactoryMonitor; + synchronized(this.beanFactoryMonitor) { + this.beanFactory = beanFactory; + } + } catch (IOException var5) { + throw new ApplicationContextException("I/O error parsing bean definition source for " + this.getDisplayName(), var5); + } +} +``` + + 之前我们说过,ApplicationContext负责创建Bean以及依赖注入的工作的。除此以外,从继承逻辑上来看,ApplicationContext也是BeanFactory的子类,但我们不应该将一个ApplicationContext对象理解为一个工厂类实例。因为从 refreshBeanFactory() 这个方法中我们能够得知,ApplicationContext是在内部持有一个DefaultListableBeanFactory实例,并且所有工厂类相关的操作都是委托给这个实例执行的。 + +上述是对ApplicationContext的补充,接下来 refreshBeanFactory() 中,我们需要重点关注loadBeanDefinitions() 这个方法。该方法就是Spring能够通过配置文件初始化Bean的秘密所在,它将根据配置信息,加载各个Bean,然后将加载好的Bean放到BeanFactory中: + +```java +protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException { + //给beanFactory实例对象获取一个Xml配置解析器, 用于解析配置文件中的BeanDefinition信息 + XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory); + + beanDefinitionReader.setEnvironment(this.getEnvironment()); + beanDefinitionReader.setResourceLoader(this); + beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this)); + + this.initBeanDefinitionReader(beanDefinitionReader); + //加载BeanDefinition的核心逻辑 + this.loadBeanDefinitions(beanDefinitionReader); +} +``` + +从目前提及的执行流程上来看,可以如下图所示: + +![image-20200221152532053](C:\Users\WHL\AppData\Roaming\Typora\typora-user-images\image-20200221152532053.png) + +到这里,初始化BeanFactory实例,以及一些加载、注册Bean的工作就完成了。我们回到 refresh() 方法,从比较重要的方法开始,继续分析。 + +下面我们继续看finishBeanFactoryInitialization() 初始化单例的逻辑 —— preInstantiateSingletons(),可以看到该方法存在于DefaultListableBeanFactory,这也印证了我们之前对ApplicationContext的分析: + +```java +public void preInstantiateSingletons() throws BeansException { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Pre-instantiating singletons in " + this); + } + // this.beanDefinitionNames保存了所有的beanName + List beanNames = new ArrayList(this.beanDefinitionNames); + // 下面这个循环,触发所有的非懒加载的 singleton beans 的初始化操作 + for (String beanName : beanNames) { + ... + // 非抽象、非懒加载的单例Bean如果配置了 'abstract = true',那是不需要初始化的 + if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) { + if (isFactoryBean(beanName)) { + .... + } + else { + // 对于普通的Bean, 只要调用getBean(beanName)这个方法就可以进行初始化了 + getBean(beanName); + } + } + } + // 到这里所有的非懒加载的单例Bean已经完成了初始化 + .... +} +``` + +可以看到 getBean() 这个方法就是我们从容器中获取Bean实例的方法,如果是lazy-load的类,我们在外部调用 getBean() 时才会执行初始化逻辑;若类的实例已经初始化了,则直接从容器中获取即可: + +```java +protected T doGetBean(final String name, final Class requiredType, final Object[] args, boolean typeCheckOnly) + throws BeansException { + //获取到真正的beanName(可能传入的是别名, 需要调用方法对别名进行转换) + final String beanName = transformedBeanName(name); + //返回值 + Object bean; + //检查是不是已经创建过了 + Object sharedInstance = getSingleton(beanName); + //之前我们在分析中看到的都是getBean(beanName), 这意味着args传参在之前的情况下是null的 + //但是如果args不为空的, 那么意味着调用方不是希望获取Bean, 而是创建Bean + if (sharedInstance != null && args == null) { + if (logger.isDebugEnabled()) { + //省略日志逻辑... + } + //如果是普通Bean, 直接返回sharedInstance + //如果是FactoryBean, 返回它创建的那个实例对象 + bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); + } + //创建Bean逻辑 + else { + if (isPrototypeCurrentlyInCreation(beanName)) { + throw new BeanCurrentlyInCreationException(beanName); + } + //检查一下对应的BeanDefinition在容器中是否存在 + BeanFactory parentBeanFactory = getParentBeanFactory(); + if (parentBeanFactory != null && !containsBeanDefinition(beanName)) { + // 如果当前容器不存在这个BeanDefinition, 试试父容器中有没有 + String nameToLookup = originalBeanName(name); + //若args为空, 说明是获取逻辑 + if (args != null) { + //返回父容器的查询结果 + return (T) parentBeanFactory.getBean(nameToLookup, args); + } + else { + //传入args参数给getBean()方法, 执行创建逻辑 + return parentBeanFactory.getBean(nameToLookup, requiredType); + } + } + if (!typeCheckOnly) { + markBeanAsCreated(beanName); + } + try { + final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); + checkMergedBeanDefinition(mbd, beanName, args); + //初始化依赖的所有Bean + String[] dependsOn = mbd.getDependsOn(); + if (dependsOn != null) { + for (String dep : dependsOn) { + //检查是不是有循环依赖 + if (isDependent(beanName, dep)) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'"); + } + //注册依赖关系 + registerDependentBean(dep, beanName); + //初始化被依赖项 + getBean(dep); + } + } + // 如果是单例类型, 则创建单例实例 + if (mbd.isSingleton()) { + ... + } + + // 如果是 prototype scope 的,创建 prototype 的实例 + else if (mbd.isPrototype()) { + ... + } + + // 如果不是singleton和prototype的话需要委托给相应的实现类来处理 + else { + ... + } + } + catch (BeansException ex) { + cleanupAfterBeanCreationFailure(beanName); + throw ex; + } + } + // 最后检查一下类型对不对, 不对的话就抛异常, 对的话就返回了 + if (requiredType != null && bean != null && !requiredType.isInstance(bean)) { + ... + } + return (T) bean; +} +``` + +总结上述流程,我们的流程执行图可以补充为如下所示: + +![image-20200221162213100](C:\Users\WHL\AppData\Roaming\Typora\typora-user-images\image-20200221162213100.png) + +说实话,Spring源码相比起 JDK 的源码,那是复杂了好几十倍,要阅读是相当困难。上面的逻辑保不准有哪些地方出错,也省略了大多数步骤... 但大致Ioc容器的初始化、以及获取bean实例的步骤都囊括了。可以将步骤简化描述为如下所示: + +1. 通过ClassPathXmlApplicationContext初始化ApplicationContext实例 +2. ClassPathXmlApplicationContext内部会执行refresh操作,这个方法包含了Ioc容器创建的核心步骤 +3. refresh()内部会先初始化beanFactory实例,并完成一些准备工作。然后再将非 lazy - load 的单例对象先加载到容器(beanFactory实例)。 +4. 结束refresh()操作,返回ApplicationContext对象实例,通过这个实例就可以对IoC容器进行各种操作了。 \ No newline at end of file diff --git "a/week_08/25/mybatis\350\277\220\350\241\214\346\227\266\345\272\217\345\233\276.md" "b/week_08/25/mybatis\350\277\220\350\241\214\346\227\266\345\272\217\345\233\276.md" new file mode 100644 index 0000000000000000000000000000000000000000..7e7dbc9d0b2682bfd57c63fab58ef16cdf57c698 --- /dev/null +++ "b/week_08/25/mybatis\350\277\220\350\241\214\346\227\266\345\272\217\345\233\276.md" @@ -0,0 +1,62 @@ +首先我们观察Mybatis获取SqlSessionFactory的代码,可以发现它首先会调用SqlSessionFactoryBuilder这个实例对象的 build() 方法,根据传入的mybatis配置文件进行SqlSessionFactory的实例创建。 + +```java +public class MybatisUtils { + private static SqlSessionFactory sqlSessionFactory; + + static { + try { + //配置文件的classpath + String resource = "mybatis-config.xml"; + //读取配置文件流 + InputStream inputStream = Resources.getResourceAsStream(resource); + //通过传入配置文件流获取sqlSessionFactory对象 + sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); + } catch (IOException e) { + e.printStackTrace(); + } + } + + //获取SqlSession静态方法 + public static SqlSession getSqlSession() { + return sqlSessionFactory.openSession(); + } +} +``` + +其中,创建SqlSessionFactory实例对象的流程图如下所示: + +![image-20200229224744609](C:\Users\WHL\AppData\Roaming\Typora\typora-user-images\image-20200229224744609.png) + +再看到上面代码的静态方法,我们需要通过SqlSessionFactory这个工厂类通过调用 openSession() 这个方法创建SqlSession实例对象。这个过程的流程图如下所示: + +![image-20200229224715323](C:\Users\WHL\AppData\Roaming\Typora\typora-user-images\image-20200229224715323.png) + +再看通过SqlSession执行sql语句的代码: + +```java +public class UserDaoTest { + @Test + public void test() { + // try-with-resource 获取SqlSession对象 + try (SqlSession sqlSession = MybatisUtils.getSqlSession()) { + // 通过sqlSession获取到UserDao实例 + UserDao userDao = sqlSession.getMapper(UserDao.class); + // 执行方法 + // 这里提一下, 这里就是典型的面向对象中的抽象特性, 调用时只需要关注方法的抽象即可 + User user = userDao.selectUser(1); + + // 第二种执行sql的方式, 不常用 + // User user = sqlSession.selectOne("com.dao.UserDao.selectUser", 1); + } + } +} +``` + +这里SqlSession执行的是UserDao接口对应的mapper文件中的某个sql语句,mybatis内部处理 getMapper() 方法的时序图如下所示: + +![image-20200229224645156](C:\Users\WHL\AppData\Roaming\Typora\typora-user-images\image-20200229224645156.png) + +上图中MapperProxy的代理对象就是对应代码第七行中的UserDao实例,从这里也能够看出Mybatis采用了动态代理的设计模式: + +![image-20200229224558398](C:\Users\WHL\AppData\Roaming\Typora\typora-user-images\image-20200229224558398.png) \ No newline at end of file