本文主要整理一些Spring & SpringBoot应用时和相关原理的知识点,对于源码不做没有深入的讲解。
右键新窗口打开可以放大。
虽然被称为生命周期,实际上指的是bean在初始化、回收期间调用了哪些方法。如果只看《Spring实战》,可以看到类似下面的图(图源:参考文献[3])
具体哪一步做了什么?其实这些方法都是可选的,自定义的bean可以实现,也可以不实现,实现里写什么似乎都没问题,参考文献[3]中的测试代码展示了这些方法在bean生命周期中的执行顺序。
但是对于Spring框架的bean,就有必要的用途了。这里没有深入研究,有兴趣可以自行读源码。
这是Spring AOP的两种实现方式。根据官方文档:
本节代码改写自参考文献[5]。
//用户管理接口 public interface UserManager { //新增用户抽象方法 void addUser(String userName,String password); //删除用户抽象方法 void delUser(String userName); }
//用户管理实现类,实现用户管理接口 public class UserManagerImpl implements UserManager{ @Override public void addUser(String userName) { System.out.println("调用了新增的方法!"); System.out.println("传入参数为 userName: "+userName+" password: "+password); } @Override public void delUser(String userName) { System.out.println("调用了删除的方法!"); System.out.println("传入参数为 userName: "+userName); } }
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import com.lf.shejimoshi.proxy.entity.UserManager; import com.lf.shejimoshi.proxy.entity.UserManagerImpl; //JDK动态代理实现InvocationHandler接口 public class JdkProxy implements InvocationHandler { private Object target ;//需要代理的目标对象 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("JDK动态代理,监听开始!"); Object result = method.invoke(target, args); System.out.println("JDK动态代理,监听结束!"); return result; } //定义获取代理对象方法 // 因为只是在main()里测试,声明为private了 private Object getJDKProxy(Object targetObject){ this.target = targetObject; return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(), targetObject.getClass().getInterfaces(), this); } public static void main(String[] args) { JdkProxy jdkProxy = new JdkProxy(); UserManager user = (UserManager) jdkProxy.getJDKProxy(new UserManagerImpl());//获取代理对象 user.addUser("admin"); } }
import java.lang.reflect.Method; import com.lf.shejimoshi.proxy.entity.UserManager; import com.lf.shejimoshi.proxy.entity.UserManagerImpl; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; //Cglib动态代理,实现MethodInterceptor接口 public class CglibProxy implements MethodInterceptor { private Object target;//需要代理的目标对象 //重写拦截方法 @Override public Object intercept(Object obj, Method method, Object[] arr, MethodProxy proxy) throws Throwable { System.out.println("Cglib动态代理,监听开始!"); Object invoke = method.invoke(target, arr);//方法执行,参数:target 目标对象 arr参数数组 System.out.println("Cglib动态代理,监听结束!"); return invoke; } //定义获取代理对象方法 public Object getCglibProxy(Object objectTarget){ //为目标对象target赋值 this.target = objectTarget; Enhancer enhancer = new Enhancer(); //设置父类,因为Cglib是针对指定的类生成一个子类,所以需要指定父类 enhancer.setSuperclass(objectTarget.getClass()); enhancer.setCallback(this);// 设置回调 Object result = enhancer.create();//创建并返回代理对象 return result; } public static void main(String[] args) { CglibProxy cglib = new CglibProxy(); UserManager user = (UserManager) cglib.getCglibProxy(new UserManagerImpl()); user.delUser("admin"); } }
比较一下两者的区别,这也是常见的面试问题。
JdkProxy | Cglib | |
---|---|---|
依赖 | 被代理对象实现了接口(所有接口的方法数总和必须>0[4]) | 引入asm、cglib jar ;不能是final类和方法 |
原理 | 反射,实现被代理对象接口的匿名内部类,通过InvocationHandler.invoke()包围被代理对象的方法 | 引入asm、cglib jar,代理类实现MethodInterceptor,通过底层重写字节码来实现 |
效率 | 创建快,运行慢(见下方说明2) | 创建慢,运行快 |
我读了一下之前所用的日志拦截器源码,发现其实现的是这节标题的接口:
class CommonInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { try { // 拦截器内部逻辑 result = invoication.proceed(); catch(Throwable e) { // 异常处理 } return result; } }
声明代理链
@Configuration public class InterceptorConfig { @Bean public CommonInterceptor serviceInterceptor() { CommonInterceptor bean = new CommonInterceptor(); return bean; } // 代理名称后缀为servie的实现类 @Bean public BeanNameAutoProxyCreator servieBeanNameAutoProxyCreator() { BeanNameAutoProxyCreator creator = new BeanNameAutoProxyCreator(); creator.setName("*ServieImpl"); creator.setInterceptorNames("serviceInterceptor"); creator.setProxyTargetClass(true); return creator; } }
查了一些资料,apoalliance包下只是aop的接口规范,不是具体的实现,不要把这里的MethodInterceptor和cglib的MethodInterceptor搞混。
注:设值注入指的是通过setter注入。
之前看参考文献[7],感觉很难懂,试着改换了一种说法:
可以通过实现ApplicationEvent类和ApplicationListener接口,进行ApplicationContext的事件处理。这是标准的发送者-监听者的模型,可以用来处理业务逻辑,将代码解耦。
但是,发送和接收实际上是同步的,如果有事务,会在同一个事务内,并不能作为异步处理机制 [8] 。
示例代码见参考文献[9]。
注:工作中我对SpringBoot是偏应用的,研究并不是很深入。
见参考文献[10]。实际的过程是借助Spring Initializer这个网络应用程序来生成SpringBoot项目。
所谓核心注解,这里指的是相对Spring本身新增的一些注解,来看看它们有什么作用。
恰好这里提到的注解,都可以打在SpringBoot的启动类(不限于启动类),用下面的代码片段来进行说明。
package com.example.demo; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.PropertySource; @PropertySource(value = "classpath:application.properties") @MapperScan("com.example.demo.dal") @SpringBootApplication(scanBasePackages = {"com.example.demo"}) @Import({DemoConfig1.class, DemoConfig2.class,}) public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
从指定的文件读取变量。示例代码会从resource目录下读取application.properties变量的值。文件的格式如下,既可以用常量,也可以用变量替换,来对不同环境的值作区分。
name=value env.name=$env.value
如何使用这个值?在要使用的地方获取即可。
@PropertySource(value = "classpath:application.properties") class TestClass { @Resource private Environment environment; @Test public void test() { String value = environment.getProperty("name")); } }
使用@Value可以把配置文件的值直接注入到成员变量中。
@PropertySource("classpath:application.properties") public class PropertyConfig { @Value("${name}") private String value; ... }
3.2.1的示例代码中,如果类上没有@PropertySource,但DemoConfig1或DemoConfig2中有@PropertySource,通过@Import可以将它们加载的变量也读出来。
@Import的作用在下文会继续介绍。
@PropertySource只能导入 .properties配置文件里的内容,对于 .yml是不支持的。看了一些文章,得出结论是yml文件是不需要注解就能导入,但是需要路径。
Springboot有两种核心配置文件,application和bootstrap,都可以用properties或yml格式。区别在于bootstrap比application优先加载,并且不可覆盖。
这实际上是一个mybatis注解,作用是为指定路径下的DAO接口,通过sqlmapping.xml文件,生成实现类。
@SpringBootApplication是由多个注解组合成的。源码如下:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { // 略 } 简单介绍一下这些注解。 #### 3.2.4.1 元注解 最上面四行都是元注解。回忆一下它们的作用<sup>[12]</sup>: * @Target 注解可以用在哪。TYPE表示类型,如类、接口、枚举 * @Retention 注解的保留时间期限。只有RUNTIME类型可以在运行时通过反射获取其值 * @Documented 该注解在生成javadoc文档时是否保留 * @Inherited 被注解的元素,是否具有继承性,如子类可以继承父类的注解而不必显式的写下来。 #### 3.2.4.2 @SpringBootConfiguration 标注这是一个SpringBoot的配置类,和@Configuration功能是相通的,从源码也可以看出它直接使用了@Configuration。 #### 3.2.4.3 @EnableAutoConfiguration 这个注解是实现自动化配置的核心注解,定义如下 ```java @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { // 略 }
借助@Import引入的AutoConfigurationImportSelector,SpringBoot应用将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器。具体的细节这里不展开了。
扫描@Service、@Repository、@Component、@Controller等标注的类,创建bean。
可以设置扫描范围,决定类哪些生成bean,哪些类不生成。
将外部资源(bean、@Configuration配置类)导入到当前IOC容器中。
使用@Import便可以实例化引用的jar中定义的bean了。
指的是在依赖中引用的各种starter包。starter可以看作是“依赖jar+配置”打包的结果,目的是降低开发者引入组件的成本,不用自己梳理依赖、编写配置文件。
starter遵循“约定大于配置”的原则,使用的组件的配置大部分都有默认值,不声明也可以直接用。
创建一个Spring boot的简易步骤(完整的可以看参考文献[14]):
通用步骤如下,其中1可能需要移除内嵌tomcat,2有其他形式,因为我工作时都是拿线程脚本打包的,没有实际操作过,下面步骤仅供参考。
本节内容结合了参考文献[16]进行补充,上面提到的知识不再重复。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.6.RELEASE</version> </parent>
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>1.5.6.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
可以不需要,内置了 Tomcat/Jetty等容器。
如何使用Jetty?排除掉Tomcat依赖并引入Jetty,并更改一些application配置。两种容器的比较和替换方式见参考文献[17]。
Bean实现接口 ApplicationRunner或者CommandLineRunner即可。
主要有两种
可以兼容,使用 @ImportResource 注解导入老 Spring 项目配置文件。
1. AOP -连接点和切点的区别
2. Spring AOP术语:连接点和切点的区别。
3. 深究Spring中Bean的生命周期
4. Spring AOP代理用的到底是CGLIB还是JDK动态代理
5. Spring的两种动态代理:Jdk和Cglib 的区别和实现
6. Spring AOP中的JDK和CGLib动态代理哪个效率更高?
7. 经典面试题-构造方法注入和设值注入有什么区别?
8. Spring的ApplicationEvent
9. spring-第三篇之ApplicationContext的事件机制
10. 使用IDEA搭建一个简单的SpringBoot项目——详细过程
11. 浅析PropertySource 基本使用
12. JAVA核心知识点--元注解详解
13. 简单讲讲@SpringBootApplication
14. Spring Boot Starters
15.SpringBoot 打包成war
16. 吐血整理 20 道 Spring Boot 面试题,我经常拿来面试别人!
17. SpringBoot2使用Jetty容器(替换默认Tomcat)