这张图片非常生动形象地描述了 IoC 的性质,它还能进行更多扩展——比如图上的女孩拿到衣服后,可以烫衣服,缝补……以此说明 IoC 容器可以对 Bean 进行非常多的操作,AOP 离开了 IoC,实现也是比较麻烦的。
虽然毕设后端使用的是 Spring Boot,但我实际上对 Spring 斌没有真正的了解过,也就能敲一些业务代码罢了(比较重要的测试也不会用)。现在为准备工作,学一些 Spring 基础,顺便做一些笔记。这里只是粗糙地了解一下,等真正熟练使用了再去了解更深层的东西。
Spring 是什么 Spring 是什么?它是什么条件下产生的,基于怎样的理念,提供了什么功能,解决了什么问题?这几个问题是重要的,但是我目前可没法全部回答。
广义上来说,我们常说的 Spring 实际上指的是 Spring 生态圈 ,它包括核心的 Spring 框架,方便配置的 Spring Boot 框架,提供分布式和微服务的 Spring Cloud,负责数据持久化的 Spring Data,负责安全的 Spring Security……其中 Spring 框架是其的核心。
Spring 是一个后端框架 ,可用于编写服务端应用,桌面端应用(哪有人这么用!),同分布式,微服务等进行结合也是容易的。Spring 是非侵入式的(但 Spring Boot 不是,作为一个脚手架,它要求项目完全在它的框架里演化)。Spring 也提供了各种功能丰富的框架——Web,响应式 Web,安全,模板,持久化,运行时监控等,其也容易和很多其它 java 库结合使用。
应用程序是由许多组件构成的,每个组件都负责一部分功能,一般来说其会通过和其他组件进行协调和交互来完成自己的任务(和 OOP 很相似)。Spring 的核心是提供了一个容器 ——Spring 应用上下文(Spring Application Context) ,其将对应用程序中各组件进行创建和管理,最终装配 到一起。被管理的组件称为** bean**。
为什么要将组件的生命周期交给 Spring 容器进行管理?为了实现所谓的控制反转(IoC,Inversion of Control) ,对程序进行解耦,同时让组件更加专注于业务——不需要负责依赖对象的创建和管理了。
这种装配的过程是通过依赖注入(DI,Dependency Injection) 实现的,对每个 bean,容器将对其的所有依赖(即其持有的其它 bean)进行注入。可以认为 Spring 应用上下文所维护的就是各个 bean(或者说各个组件)的生命周期及其相互关系。
装配的配置可以使用 xml 或 java 类,但最常用的仍旧是自动装配——Spring 自动发现包中的组件并进行装配。
IoC 是什么以及其的意义
我们自己每次用到什么依赖对象都要主动地去获取,这是否真的必要?我们最终所要做的,其实就是直接调用依赖对象所提供的某项服务而已。只要用到这个依赖对象的时候,它能够准备就绪,我们完全可以不管这个对象是自己找来的还是别人送过来的 ……
—— 《Spring 揭秘》
IoC(控制反转)即对所需对象的创建不由程序进行,而是由用户进行,将控制权交由用户,Spring 中的 IoC 即是将对象的创建,管理交由 Spring 提供的 IoC 容器进行,其实现的方式是 DI(依赖注入)。IoC 可以认为是工厂方法模式的一种应用。
就如大多数特定的技术一样,IoC 的意义也在于对程序解耦合,提高程序的可维护性。
考虑这样一个场景,在某个 Service 里通过 DAO 层获取用户信息。这里系统使用了 MySQl 作为数据库。于是得到了这样的代码——
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 interface UserService { fun getUser () : User; }interface UserDao { fun getUser () : User; }class UserDaoMySQLImpl : UserDao { override fun getUser () : User { println("查询了 MySQL" ) return User() } }class UserServiceImpl : UserService { private val userDao : UserDao = UserDaoMySQLImpl() override fun getUser () : User { return userDao.getUser() } }
然后,出于某种奇怪的业务需要,系统又换成了 SQL Server 数据库,然后下面的程序员就编写了 UserDaoSQLServerImpl,并把各种 Service 里的所有的 UserDaoMySQLImpl 改成 UserDaoSQLServerImpl。
再然后,需求又改了,要用 sqlite 数据库了,于是程序员们又重复了上面的步骤,周而复始……
这里就可以看到问题,每次需求一改变,都要改源代码,这里的问题在于,各种 serviceImpl 实际上和各种 daoImpl 是紧耦合 的,要更换使用的 dao 就必须侵入式地修改代码,显然这里可以使用工厂方法模式,通过反射和配置文件生成相应的类。
但是这里先不提工厂方法模式,一个比较简单的修改方式是,将使用哪个 daoImpl 的权力交给“用户”(使用相关类的代码),让用户通过构造器或 set 方法,对想要使用的 dao 进行设定。如下面的形式——
class UserServiceImpl : UserService { private var userDao : UserDao? = null fun setUserDao (userDao : UserDao ) { this .userDao = userDao; } override fun getUser () : User { return userDao?.getUser()!! } }fun userCode () { val userService : UserService = UserServiceImpl() (userService as UserServiceImpl).setUserDao(UserDaoMySQLImpl()) userService.getUser() }
用户想用 mySQL?想用 SQL Server?自己选择相应的实现就是了!这就是所谓的控制权交给用户,即控制反转的一个示例。更换实现也不需要改变源代码了。但是显然,这也是比较复杂和麻烦的——用户需要手动注入所需依赖,且需要自己创建所需的实际对象,引入了本不必要的复杂性,抽象程度降低了。“如果有人能够在我们需要时将某个依赖对象送过来,为什么还要大费周折地自己去折腾?”
IoC 的使用 在 Spring 中,对象创建的控制权交给了 IoC 容器,被接管的对象称为** Bean**。用户需要通过 xml,Java 代码或注解对 Bean 进行配置。使用时,根据 xml 或其它形式获取相关配置的上下文,并手动或自动对 Bean 进行获取,下面是一个手动获取 Bean 的示例。
IoC 容器接管的类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 interface HelloDao { public void getHappyEnergy () ; }public class HelloDaoImpl implements HelloDao { public void getHappyEnergy () { System.out.println("Happy, Lucky, Smile, Yeah!" ); } }interface HelloService { public void getHappyEnergy () ; }public class HelloServiceImpl implements HelloService { private HelloDao helloDao; public void setHelloDao (HelloDao helloDao) { this .helloDao = helloDao; } public void getHappyEnergy () { helloDao.getHappyEnergy(); } }
xml 文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?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:p ="http://www.springframework.org/schema/p" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" > <bean id ="helloDao" class ="xyz.yukina.dao.HelloDaoImpl" /> <bean id ="helloService" class ="xyz.yukina.service.HelloServiceImpl" p:helloDao-ref ="helloDao" /> </beans >
用户代码:
public static void main (String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext ("ApplicationContext.xml" ); HelloService helloService = context.getBean("helloService" , HelloService.class); helloService.getHappyEnergy(); }
可见,现在用户要获取的实例是通过配置文件给定的,而对配置文件的修改是不需要修改源代码的,这就让各个模块解耦了——在使用 IoC 之前,各个模块是紧密相连,需要手动处理(new)相互的依赖关系,使用 IoC 容器后,它们实际上只需要与 IoC 容器交互 即可,这样互相的耦合性就降低了,且用户也可以不必关心自己究竟用的是哪个具体类,提高了系统的抽象性。且将对象的创建和管理交管给 IoC 容器,这也让容器对对象进行特定的处理也成为可能,或许 AOP 就是在此条件下实现的吧!
需要注意的是,Bean 默认是单例 的,非惰性 的——在执行new ClassPathXmlApplicationContext("ApplicationContext.xml")
时,所有 bean 默认将被初始化而无论是否使用。
关于注入的方式 Spring 提供的注入方式有三种——域注入,set 注入,构造器注入。默认在 xml 中进行配置的话使用的是 set 注入和构造器注入,将 Autowired 注解作用到域中则是域注入。Spring 不推荐使用域注入。
Spring 推荐使用构造器注入,其次是 set 注入,其中构造器注入用于注入必需的(不为 null)依赖,set 注入用于注入可选的,或者给定默认值的依赖。kotlin 推荐的方式也是构造器注入。构造器注入的优势在于其能够定义注入对象为 final(或者 kotlin 的 val),语义更加明确。
其中,对于构造器注入,Spring 推荐使用断言来保证为非 null(这里姑且使用 lombok 提供的 NonNull 注解)。一个示例如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class HelloServiceImpl implements HelloService { @Autowired private AbcDao abcDao; private AnotherDao anotherDao; @Autowired public void setAnotherDao (AnotherDao anotherDao) { this .anotherDao = anotherDao; } private HelloDao helloDao; @Autowired public HelloServiceImpl ( @Qualifier("HelloDaoImpl") @NonNull HelloDao helloDao // 如果有歧义,可以在这里用 Qualifier 注解 ) { this .HelloDao = helloDao; } public void getHappyEnergy () { helloDao.getHappyEnergy(); } }
需要注意的是,构造器注入法可能导致循环依赖问题!
静态代理
关于 AOP,我曾经以为它意义并不重大,直到最近我发现,很多地方如 Spring MVC 中都使用了 AOP,且一些可能对业务很重要的地方也是通过 AOP 实现的。比如控制器中方法的返回值将会经由切面转换成相应视图,JSON 或其它对象,这个转换是可以通过自定义切面进行自定义的,可见学习 AOP 是比较有意义的。
Spring 的 AOP 的实现利用了代理模式 。实际上我在编写毕设时已经接触过了代理模式——vue 的 ref 对象将原对象的 get 和 set 方法进行劫持 (我觉得这个词更加形象)以保证监听数据改变,这样,在操作 ref 对象的时候,我们采用和普通对象一样的方式,而 vue 则在后台做更多的操作,如更新视图,维护相关计算属性,方法,监听器等。
在这里,代理模式的作用在于,真实角色(即原对象)的操作完全不需要关心任何视图层的事情,简单纯粹,而相应的业务交由代理角色(ref 对象)执行,容易进行维护和扩展。代码的编写者只需要对代理角色像真实角色一样进行操作即可,摆脱了手动管理 DOM 的麻烦。我认为代理模式最大的意义在于,通过特定的抽象,客户端可以完全不用关注自己所使用的是真实角色还是代理角色,从而提高了系统的抽象程度 。
代理模式有四个角色:
接口
接口代表真实角色需要进行代理的方法。在 vue 中,这些方法是对象的各个属性的 get 和 set,因此这里的接口角色是隐含的。
真实角色
真实角色即为被代理的角色,它应当实现接口。
代理角色
代理角色应当持有真实角色(通过组合 和继承均可,组合最优,这时就通过委托 进行代理;这里我认为使用继承在语义上更加符合)并实现接口,在接口中,除调用真实角色的相应方法,也应加入自身的处理。代理角色和真实角色都实现接口,以保证其提供的 API 一致。
客户端
客户端为调用接口的角色,在这里,客户端应当使用代理角色,并期待它通过真实角色进行操作时同时执行自己的业务。
AOP 其实就是这样的东西——它对特定对象进行切入 ,在这些对象执行特定方法时能够进行“劫持”,添加进自己的操作,比如输出,计数,监控 ,发给某些企业 ,异常处理(!)等,甚至可修改传递给原对象的方法的参数。将方法标识为事务也是利用了 AOP。下面编写了一个实例——通过代理对原有操作进行计数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 interface MyService { void doSomething () ; }class MyServiceImpl implements MyService { @Override public void doSomething () { System.out.println("做点什么事" ); } }class ServiceProxy implements MyService { private final MyService myService; private int counter = 0 ; public ServiceProxy (MyService myService) { this .myService = myService; } @Override public void doSomething () { System.out.println("第" +(++counter)+"次执行" ); System.out.println("执行前" ); myService.doSomething(); System.out.println("执行后" ); } }public class Main { public static void main (String[] args) { MyService myService = new MyServiceImpl (); MyService myServiceProxy = new ServiceProxy (myService); myServiceProxy.doSomething(); } }
动态代理 上面所说的是静态代理模式——代理角色是写死的,而还存在动态代理模式——动态生成代理角色(vue 的例子应当也是动态代理)。动态代理分为基于接口的动态代理 和基于类的动态代理 。
动态代理其实就是利用反射对方法调用进行劫持。这里展示了一个代理 handler 的例子,它劫持所有方法调用,在方法执行前和执行后都进行输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class ProxyInvocationHandler implements InvocationHandler { public static Object getProxyInstance (Object obj) { return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), new ProxyInvocationHandler (obj)); } private Object t; public ProxyInvocationHandler (@NotNull Object t) { this .t = t; } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { System.out.println("执行前" ); Object res = method.invoke(t, args); System.out.println("执行后" ); return res; } }public static void main (String[] args) { UserService serviceProxy = (UserService) ProxyInvocationHandler.getProxyInstance(new UserServiceImpl ()); serviceProxy.add(); }
代理类的接口为什么要这么设计?看上去简直就是在说,代理处理器是和特定方法(接口)绑定的,而非是和实例绑定的,但在实践中却是和实例进行绑定的。
这个代理 handler 可以进一步抽象成更加一般的形式——要求用户给定方法执行前,执行后将要执行的代码,给定对 args 的处理,给定对输出结果的处理……这里按本人想法进行了一些编码,可以意识到,invoke 方法采用了模板方法模式。相信 Spring 的 AOP 对此有更加具有实践性和更优雅的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public abstract class ProxyHandler implements InvocationHandler { private final Object obj; public ProxyHandler (Object obj) { this .obj = obj; } public void before () {}; public void after () {}; public Object[] parseArg(Method method, Object[] args) {return args;}; public Object parseRes (Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {return method.invoke(obj, args);}; @Override public final Object invoke (Object proxy, Method method, Object[] args) throws Throwable { before(); Object res = parseRes(method, parseArg(method, args)); after(); return res; } public final Object getProxyInstance () { return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this ); } }public class CounterProxy extends ProxyHandler { public CounterProxy (Object obj) { super (obj); } private long startTime; private long endTime; @Override public void before () { System.out.println("执行前" ); startTime = System.currentTimeMillis(); } @Override public void after () { endTime = System.currentTimeMillis(); System.out.println("执行后" ); System.out.println("程序运行时间: " +(endTime-startTime)+"ns" ); } }public static void main (String[] args) { UserService service = (UserService) new CounterProxy (new UserServiceImpl ()).getProxyInstance(); service.add(); service.delete(); }
AOP 的具体使用方法不再研究了,先研究其他更重要的东西去。