文章目录
Spring 基础
Ioc
有什么好处
-
使用者不需要关注对象的构建过程,我们只要借助ioc提供的方法,然后使用就可以了
-
如果我们的系统有特别复杂的依赖关系,spring也能够帮助我们自动管理
-
基于接口注入,实现类想换哪个就换哪个,系统的耦合度低
设计
主要基于BeanFactory
和ApplicationContext
两个接口
其中BeanFactory
是Ioc最底层的接口,但是ApplicationContext
对它做了很多有用的扩展,所以大部分我们会时候后者
BeanFactory
public interface BeanFactory {
String FACTORY_BEAN_PREFIX = "&";
//下面是几个获取bean的方法,可以通过名称和类反射来获取
Object getBean(String name) throws BeansException;
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
Object getBean(String name, Object... args) throws BeansException;
<T> T getBean(Class<T> requiredType) throws BeansException;
<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;
<T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);
<T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);
boolean containsBean(String name);
//单例模式
boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
//与单例模式相反
boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;
//按type类型匹配
@Nullable
Class<?> getType(String name) throws NoSuchBeanDefinitionException;
@Nullable
Class<?> getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException;
//获取别名
String[] getAliases(String name);
}
其实spring官方的源码注释还是相当详细的,如果你看到的是一些奇奇怪怪的变量名,比如var1,var2,你试着下载源码,这个时候看到的才是spring的源码。比Mybatis注释详细的多,我一度怀疑自己没有下载mybatis的源码。太详细了,再夸一句spring牛逼
ApplicationContext
翻译一下:应用的上下文
ClassPathXmlApplicationContext
其中的一个实现类,负责读取类路径下的xml配置的bean信息
Spring Ioc 初始化过程
先定义再初始化
定义的步骤:(整个过程是没有进行任何初始化的)
- Resource 资源定位,通过开发者的配置或注释,定位到资源也就是我们定义Bean的地方
BeanDefinition
,把定位的Bean信息保存到BeanDefinition
BeanDefinition
的注册,把里面的信息发布到Ioc容器中
注意,完成这些步骤,Bean只是在容器中被定义了,但是没有完成初始化,更没有什么依赖注入,也就是没有把配置的信息放到Bean里面
Spring Bean 有一个配置选项lazy-init
,是否延迟初始化,如果延迟初始化,那么只有我们在Ioc容器里面获取它的时候,才会进行Bean的初始化,完成依赖注入
关于切面的其他收获
Ioc容器的本质就是为了管理Bean,如果我们的bean在初始化或者销毁的时候,需要一些自定义的过程,那么我们就需要用到Bean的生命周期
Idea帮助我们自动填充的代码,比如我们在@Override一个方法的时候,idea会帮助我们调用上层的方法,代码还是比较臃肿的,我们可以看看上层是怎么实现的,只要和上层做一样的实现,处理自己事情就可以了
还是有点难的,不过我也收获了不少
我为了偷懒,想着用切面查看一下这些方法执行前后的参数,所以定义了一个切面类
@Aspect//切面注解
@Component//让spring管理
@Slf4j//日志
public class MethodAspect {
@Around(value = "@annotation(methodLog)")
//首先要注意的就是这个指定,这个是下面方法参数的参数名,所以是小写的
public Object doAround(ProceedingJoinPoint joinPoint, MethodLog methodLog) throws Throwable {
String name = methodLog.value();//通过注释我们获得的属性
long start = System.currentTimeMillis();
//以下的这些方法,是无法获取你给方法参数起的名字是什么
Object aThis = joinPoint.getThis();//得到执行这个方法的对象本身
Object[] args = joinPoint.getArgs();//得到方法传入的参数值
Signature signature = joinPoint.getSignature();//得到方法的签名,类名 方法名 方法参数类型
//我把上面这个打印出来,才真正的理解了签名是什么意思,就是我们可以通过签名唯一的定位一个方法
Object target = joinPoint.getTarget();//返回的结果
log.info("{} : {}",name,target);
// log.info("{} {} {} {}",args,aThis,joinPoint.getTarget(),name);
try {
return joinPoint.proceed();//让真正的方法执行
} finally {
long t = System.currentTimeMillis() - start;//这个可以统计一个方法执行说需要的时间
}
}
//后来我发现,真不如去看源码,如果你看了函数还猜不出方法是什么意思,可以直接看源码的注释,这是spring最方便的一点,没有它,我们想要学会这个框架,难度还是非常离谱的
}
不过这种方法其实是不行的,因为spring boot对bean的拦截太多了,到处都充满了它自己的类,所以我们还是得按照书上的方法,老老实实来,下面我就直接给出一个尽可能详细的注释,来描述bean的整个生命周期
@Component
public class InnerBean implements BeanPostProcessor {
@Override//对象开始示例化
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override//对象实例化完成
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
Bean的生命周期
-
初始化
-
依赖注入
-
setBeanName
BeanNameAware
接口 -
setBeanFactory
BeanFactoryAware
接口- 4和5一般是spring自己用的方法,用来获取bean和bean名称
-
setApplicationContext
ApplicationContextAware
接口- 需要容器实现
ApplicationContextAware
接口,它才会起作用 - 主要是这里有
ApplicationContext
作为接口参数
- 需要容器实现
-
postProcessBeforeInitialization
- 针对所有bean而言
-
afterPropertiesSet
InitializingBean
接口 -
@PostConstruct
- 自定义初始化方法
-
postProcessAfterInitialization
- 针对所有bean而言
-
生存期。。。。。。。
-
destroy
DisposableBean
接口 -
@PreDestory
- 自定义消耗方法
Bean 装配
构造器注入
利用构造器,反射注入,不推荐,因为参数多的话,可读性不好
setter注入
利用无参数构造器,结合set方法构造,推荐
接口注入
用于注入系统外界的配置
spring 提供的3种配置方法
- xml配置(基本废弃)
- java接口或类(代替xml完成复杂的配置)
- 隐私Bean发现机制和自动装配原则(推荐,优先使用)
xml配置
如果我们需要配置的存在不定形式的嵌套,也可能不嵌套,spring用ref标签实现了这样的设计要求
spring在xml配置里,使用过命名空间,有点类似与vue在html标签里扩充的属性,简化了我们编写,但其实可读性才是最重要的,因为是xml唯一的优势,还是一个优势是标签的互相引用,可以介绍大量重叠配置,而且可以自由嵌套
注解装配
组件扫描
定义资源,让spring扫描特点的包
自动装配
通过注解来定义,使得一些依赖关系可以通过注解来完成
@Component
这个注解有属性value,代表它在spring中的id,如果没有指定就是类名的首字母小写
@ComponentScan
在spring boot的自动配置下,这个注释不是很常用,但可以帮助我们扫描,spring boot默认没有扫码到的包,它默认会扫码这个包下的其他的类,同时也可以指定包扫描的位置
如果多次指明要扫码的包,会导致重复创建代理对象,这个是很麻烦的,不要出现这种情况,除非你真的需要,所以在spring boot中,我们选择了@Component
两种扫码机制,如果你类的位置固定不动,那么用包扫描,可读性好,否则用类扫描,适合于那种类位置,包变动的
AnnotationConfigApplicationContext
spring 用这类来实现从注释中获取bean
@Autowired
当spring完成某个Bean的定义和生成,它会寻找被@Autowired注释的资源,然后从自己的Bean中找到对应的,把它注入进去
spring推荐我们用接口注入一个bean,这个bean只会出现接口可以调用的方法,如果想让自己在类中定义的方法,在接口中出现,我们可以重写这个方法@Override然后idea会帮我们把这个方法定义在接口中
这个注释可以加在set方法中,如果加载set方法,可以做一些自定义的代码,一般情况是不需要的
构造方法可以使用@Autowired,不过这个注解要写在参数上面
如果一个接口有多个实现类,@Primary
标识一个主要的,如果没有具体指定的话,会使用这个类来注入,比如默认数据源。
@Qualifier
,在BeanFactory
接口中,有按名称注入的接口,但是spring默认采用按类型注入,这个名称就是@Component
中定义的名称,没有指定的话,就得是类名首字母的小写
@Bean
第三方包的类怎么处理,有的第三方包把扩充的余地交给了我们,我们可以定义一个类,继承第三方包指定的类或实现第三方包指定的接口,然后用@Component注解,但大部分情况,我们更希望这些配置在yaml中,除非他需要一些逻辑,写在代码中更加简单一些
但,spring还给我们提供了一种注解,让我们在一个类的方法中引入第三方包,它在方法上注解,然后将返回值作为spring的bean,而可以使用Bean的name属性指定名称,而且可以通过注解指定创建和销毁要执行的方法
xml中也可以定义bean,然后通过@ImportResourse
引入
@Import
我在spring boot中经常看到这个注解,这个用来引入多个配置类
Profile
环境隔离,比如在开发环境,测试环境,线上环境使用不同的数据库,但是配置之后,这些bean需要我们通过指定环境参数来激活,负责是不会加入到Ioc容器的
- SpringMVC,可以配置web上下文,或者DispatchServlet参数
- 作为JNDI条目
- 配置环境变量
- 配置JVM启动参数
- @ActiveProfiles
@ActiveProfiles
指定环境的名称,然后注入相应的bean
环境变量
spring.profiles.active 配置后spring.profiles.default失效,应该默认环境是开发环境,在后端上线的时候,通过脚本指定好环境,直接启动,覆盖掉其他人员的默认配置
JVM
JAVA_OPTS="-Dspring.profiles.active=test"
条件化装配和属性配置
我影响中spring boot对这个地方做了升级,属性用yaml,条件也提供的新的注解,我们会写在spring boot的专属笔记中
作用域
@Scope
注解,可以指定bean的作用域
singleton 单例
默认作用域
prototype 原型
获取一次装配一个新的实例
session 会话
一次会话过程创建一次
request 请求
一次完整的请求对应一个
Sring EL
spring非常强大的表达式解析器,可以完成属性的复杂装配,比如配置文件的值和我们想要的值有一定的区别,但是为了配置文件的可读性,为了配置文件的简易性,我们可以用 Sring EL
面向切面编程
术语概念
Aspect 切面
指定切面的工作环境,比如controller通配符,自定义注解
Advice 通知
before
前置通知,在执行原有方法之前执行
after
后置通知,执行完方法后,无论方法是否出现异常都会执行
afterReturning
返回通知,无异常时执行,比如事务提交
afterThrowing
异常通知,出现异常时执行,比如事务回滚
around
环绕通知,可以看成前面几个通知的组合写法(推荐)
Introduction 引入
容许我们扩充现有的类,让它有更多的方法
Pointcut 切点
告诉spring在什么时候切入
join point 连接点
具体要拦截的,比如一个方法,还有方法的签名,参数等
Weaving 织入
生成代理对象,并且将切面内容放入到流程中
静态代理:在编译class文件的时候生成逻辑代码
动态代理:ClassLoader
类加载的时候生成的逻辑代码,但是在代码运行前生成,CGLIB,spring动态代理,运行期间生成
注意:spring是方法级别的AOP
AOP的使用
@Aspect
注解在类上,表示这个类定义了切面的细节
指示器
指示器可以指定哪些方法被拦截
execution 正则表达式匹配
@annotation 注解
args(xxx,yyy)可以把方法中名为xxx和yyy的参数直接传递给这个切面方法
ProceedingJoinPoint
我们想了解一个类中的属性代表的值是什么含义,最好的方法是,看文档,看博客,或者用debug去实际的方法中对照一些,这个值最好你可以用到的,可以获取的值,否则是没有什么意义的,一般框架肯定会把有用值的get方法提供出来,所以,不要自通过一个一个打印get方法的值,去猜测了
织入
如果我们类实现了一个接口,那么spring会使用jdk的动态代理,否则spring会使用CGLib的代理,所以我们是否定义了接口,和AOP是没有任何关系的
@DeclareParents
让一个类被动的实现一个接口,然后我们可以在切面中调用这个接口提供的方法,通过强制类型转换就可以
xml配置
由于这个没什么人用,所以不做笔记,但是记录一下,因为xml中使用一下符号,比如&&,||,!,<,>会有一定的问题,所以spring在这里做了转义,比如用and,or,not来替代,这个地方可以我们可以学习一下,而且单词的可读性更好
多个切面
@Order
在spring中,一切涉及到顺序的问题,几乎都可以用这个注解来解决
执行顺序
就是责任链的执行顺序,一层套一层
不得不说,书讲的实在太少了,可以作为入门算是不错的书,至少我能看懂
数据库编程
JDBCTemplate
spring会给常用的技术提供模板化的支持,这也是一种设计模式
技术不常用,不做赘述
MyBatis-Spring
将mybatis和spring结合起来
public class SqlSessionFactoryBean
implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
private static final ResourcePatternResolver RESOURCE_PATTERN_RESOLVER = new PathMatchingResourcePatternResolver();
private static final MetadataReaderFactory METADATA_READER_FACTORY = new CachingMetadataReaderFactory();
private Resource configLocation;//MyBatis的配置文件
private Configuration configuration;//mybatis的全局配置对象
private Resource[] mapperLocations;//mapper配置路径
private DataSource dataSource;//数据库
private TransactionFactory transactionFactory;//事物管理器
private Properties configurationProperties;//配置属性
private SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();//四大对象
private SqlSessionFactory sqlSessionFactory;//sqlSession的工厂,四大对象
private String environment = SqlSessionFactoryBean.class.getSimpleName();
private boolean failFast;//是否快速失败
private Interceptor[] plugins;//插件
private TypeHandler<?>[] typeHandlers;//类型转换器
private String typeHandlersPackage;//类型装换器在的包
@SuppressWarnings("rawtypes")
private Class<? extends TypeHandler> defaultEnumTypeHandler;//默认枚举处理器
private Class<?>[] typeAliases;//别名处理器
private String typeAliasesPackage;//别名处理器所在的包
private Class<?> typeAliasesSuperType;
private LanguageDriver[] scriptingLanguageDrivers;//数据库厂商标识
private Class<? extends LanguageDriver> defaultScriptingLanguageDriver;
private DatabaseIdProvider databaseIdProvider;
private Class<? extends VFS> vfs;//unix文件操作
private Cache cache;//缓存
private ObjectFactory objectFactory;//对象工厂
private ObjectWrapperFactory objectWrapperFactory;
}
MapperFactoryBean
怎么把mybatis对接口的动态代理和spring的动态代理结合起来,就是这个类起的作用
书中讲的特别浅,在spring boot的时代下,已经过时了,不过作为一步入门的书,我觉得还是非常好的,至少我非常有兴趣能看下去,而且学到了不少东西
事务管理
这块我一直没有遇到复杂的应用场景,所以打算好好学习一下
PlatformTransactionManager
public interface PlatformTransactionManager extends TransactionManager {
//创建事务
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
//提交事务
void commit(TransactionStatus status) throws TransactionException;
//回滚事务
void rollback(TransactionStatus status) throws TransactionException;
}
DataSourceTransactionManager
@Transactional 声明式事务
配置项 | 备注 |
---|---|
isolation | 隔离级别,非常重要,后面会详细说 |
value/transactionManager | 一个需要实现PlatformTransactionManager接口的Bean |
propagation | 传播行为,设计到事务方法之间的相互调用 |
timeout | 超时时间 |
readOnly | 只读事务 |
(no)rollback | (不)会回滚的事务类型 |
(no)rollbackForClassName | (不)同上,但用的是字符串 |
这个注解让我收获到一点,注解是可以直接配置枚举类的
原子性 (原子不可分割)
整个事务的所有操作,要么全部成功,要是有一个失败,其他所有的都会变回原来未执行事务的状态
一致性
读写一致
隔离性
事务的隔离程度,多个事务
持久性
事务完成后,该事务对数据库所做的更改会被保存
丢失更新
- 两个事务同时开启,其中一个事务发起回滚(mysql基本不会出现这种情况)
- 两个事务同时操作同一个数据和java并发修改同一个数据的影响一样
隔离级别
事务隔离级别,多个事务同时操作一个数据的时候涉及
看别人的博客,写的比书上的好MySQL相关问题
如何选择隔离级别
依据:看性能和数据的一致性哪个对你的这个业务更重要
要求数据库高度精确不能出错而且性能要求还特别高,比如秒杀系统,是不会通过隔离级别来控制数据库的一致性的
此外,在大部分环境中,还选择读/写提交
的隔离级别,这种有助于提高并发,而且压制了脏读
如果我们业务的并发特别小,或者是那种对性能要求不是很高,但是要保证数据一致性的,我们可以才用序列化的方法,保证数据库不出错
如果我们不指定@Transactional的隔离级别,他会采用默认的隔离级别,数据库默认的隔离级别,也就是说,对于MySQL是可重复读
但是对Oracle这种只支持读/写提交
和序列化
两种的,支持的是读/写提交
传播行为
方法之间的调用事务策略的问题
传播行为 | 含义 |
---|---|
REQUIRED | 默认传播行为,如果当前不存在事务,就开新事务,否则,就用之前的事务 |
REQUIRES_NEW | 无论什么情况都会新开启一个事务 |
NESTED | 和REQUIRES_NEW类似,但是可以回滚到指定的保存点 |
事务失效
因为@Transactional的底层实现是AOP,所以static和非public方法是无法使用的,static一般我们是不会用的,但是我们有注意,如果我们调用的是私有方法,那么就得小心了,调用私有方法肯定是同层之间调用,注意不要在私有方法上用@Transactional。另外,这也告诉我们,AOP是不支持这些行为的,而且最关键的是自调用的问题,就是一个类的函数互相调用,基于AOP的实现原理,同一个类的方法直接调用,是不会出现代理类的,所以会出现事务失效,所以,AOP只支持,不同类的公共的非静态方法
那怎么解决,其实很好解决,我们有为业务专属的service,一般而言,业务专属service操作好几个数据库,但是单个service只操作和自己精密相关的数据库,和DDD比较类似,但是我这种可以直接操作数据库的修改,而不是通过对象来修改,所以业务层和普通service是一个层级的,我们可以把mapper注入到业务service,BDD的核心就在于,能让对象来操作的,就让对象来操作
我们可以通过查看mybatis的日志,Creating a new SqlSession
来看事务的开启情况是什么样的
此外,如果我们想让两个在一个事务中,就不能在controller层写方法了,而且我现在觉得是真的不好,而且没什么用
事务的粒度也是需要我们把握的,因为事务和锁一样,是对影响速度影响比较大的,所以我们应该想办法缩小事务和锁的粒度,你要记住,io和数据库是非常可怕的,你要尽可能的减少对他们的使用,要多用内存,redis,怎么解决,分开方法就行,小问题。而且你要注意的是,分隔的类,只要是不同类之间的互相调用,就可以避免事务出现问题,并不是说,需要你加多少多少层
异常处理,你捕获的异常,是你要真正能处理了的,如果你不行,那么你必须处理后继续抛出,在spring中,如果你自己处理了异常,但是不抛出,就会出现spring不会帮助你回滚事务的情况