- Spring常用注解
- Spring Bean的生命周期
- Spring配置文件相关
- Spring动态代理三种使用方式
- Spring事务相关问题
- Spring注册Bean的几种方式
- Spring集成jdbc
- Spring集成Mybatis
- Spring依赖注入的三种方式及优缺点
- Spring三级缓存
更多内容见:全球顶级java开发面试宝典
Spring常用注解
@Configuration标记类为配置类,配合@Bean进行IOC Bean的装配。
@Configuration(proxyBeanMethods = true)
public class AppConfig {
@Bean
@Profile("dev") // dev环境生效这个bean
public OrderService orderService(){
return new OrderService();
}
@Scop用来指定Bean的作用域。
Bean作用域有SINGLETON(默认的单例模式),PROTOTYPE(多例,每次用到新建一个),REQUEST(每次请求用到只新建一个),SESSION(每次会话中只新建一个),GLOBLE-SESSION
@Controller
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class UserController {
}
@Lazy注解,@Configuraiton和@lazy 一起使用时,意味着所有使用@Bean的方法都是懒加载,@Bean和@Lazy一起使用则对于该Bean懒加载。懒加载在使用时才会进行加载,可解决一些循环依赖的场景。
@Lazy
@Configuration
public class AppConfig {
@Bean
// @Lazy
public Country getCountry(){
return new Country();
}
}
@Conroller @Service @Repository @Component分别作用在不同的分层,作用都是标识类为Bean,在扫描时注入容器中。
@PostConstruct标识的方法在生命周期中的依赖注入后进行执行,而@Predstroy是在销毁时执行。
@Autowired在注入Bean时默认使用byType进行注入,当有多个此类型的Bean时可以使用@Qualifier来指定byName进行Bean注入
public class People {
@Autowired
@Qualifier("cat2")
private Cat cat;
}
Spring Bean的生命周期
1、推断构造方法实例化
a、有一个有参没有无参,走有参并给要传的参数getBean赋值,先byType再byName
b、有多个有参没有无参,不知道走哪个会报错
c、有无参构造方法不管有没有有参的默认走无参的构造方法,可以用@Autowired指定构造方法
2、依赖注入
进行成员的注入。注入其他Bean而去实例化其他Bean,循环依赖多产生于此。而由构造方法循环依赖问题三级缓存不能解决。
3、Aware等其他回调
提供的各种xxxAware方法在此时进行回调。
4、初始化前
BeanPostProcessor接口方法的调用。@PostConstruct方法的调用等。
5、初始化
InitializingBean等接口方法的一些调用
6、初始化后
BeanPostProcessor接口方法的调用。
进行AOP等。
Spring配置文件相关
在xml中对Bean各种数据类型的注入
<bean name="student" class="com.xin.pojo.Student">
<!--value注入 值-->
<property name="name" value="小"/>
<!--ref注入bean 对象-->
<property name="address"ref="address"/>
<!--array注入 数组-->
<property name="books">
<array>
<value>计算机网络</value>
<value>操作系统</value>
<value>深入理解java虚拟机<value>
</array>
</property>
<!--list注入 List-->
<property name="hobbies">
<list>
<value>写代码</value>
<value>玩游戏</value>
<value>看书</value>
</list>
</property>
<!--map注入 Map-->
<property name="card">
<map>
<entry key="身份证"value="43022320010826801X/>
<entry key="银行卡"value="a66778899"/>
</map>
</property>
<!--set注入 Set-->
<property name="games">
<set>
<value>穿越火线</value>
<value>原神</value>
<value>王者荣耀</value>
</set>
</property>
<!--空值注入 null-->
<property name="wife">
<null/>
</property>
<!--props注入 Properties-->
<property name="info">
<props>
<prop key="角色">影<prop>
<prop key="武器">薙草之稻</prop>
<prop key="圣遗物">绝缘之旗印</prop>
</props>
</property>
</bean>
PC命名空间
P即(property):注入时在属性上注入。
C即(constructor):注入时在构造器上注入
<!--需要导约束-->
<bean id="user" class="com.xin.pojo.User" p:name="小红" p:age="20"/> <!--p命名空间(property):标签处实现值注入-->
<bean id="user2" class="com.xin.pojo.User" c:name="小白" c:age="20"/> <!--c命名空间(constructor):标签处实现构造器注入-->
注入方式和作用域等
byType通过类型注入。
byName通过名字注入。
scope指定作用域。
<bean id="people" class="com.xin.pojo.People" autowire="byName" scope="request">
<property name="name" value="小小饲养员"/>
</bean>
<bean id="people2" class="com.xin.pojo.People" autowire="byType" scope="session">
<property name="name" value="小小饲养员2"/>
</bean>
Bean继承
<bean id="BaseCustomerMalaysia" class="com.yiibai.common.Customer">
<property name="country" value="Malaysia" />
</bean>
<bean id="CustomerBean" parent="BaseCustomerMalaysia">
<property name="action" value="buy" />
<property name="type" value="1" />
</bean>
Spring动态代理三种使用方式
1、定义一个类并注册,在里面写好对应的before,after增强方法。在xml中可以指定这个类为切面,去对自己想增强的切点进行指定aop:before,aop:after等的增强。
public class DiyPointCut {
public void before(){
System.out.println("diyBefore");
}
public void after(){
System.out.println("diyAfter");
}
}
<bean id="diy" class="com.xin.diy.DiyPointCut"/>
<aop:config>
<aop:aspect ref="diy"> <!--切面,对切入点实现操作-->
<aop:pointcut id="diyPontCut" expression="execution(* com.xin.service.UserServiceImpl.*(..))"/>
<aop:before method="before" pointcut-ref="diyPontCut"/><!-- 主动指定生效的时机,可以直接指定切点 -->
<aop:after method="after" pointcut-ref="diyPontCut"/>
</aop:aspect>
</aop:config>
2、定义n个类并注册,实现MethodBeforeAdvice,AfterReturningAdvice等接口重写其中的方法。在xml中通知到对应的切点里,通过接口来识别增强的时机进行方法增强。
public class AfterLog implements AfterReturningAdvice { //方法返回后
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("返回了"+returnValue+"结果,后置AOP实例");
}
}
public class BeforeLog implements MethodBeforeAdvice { //方法开始前
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("此处前置AOP示例");
}
}
<bean id="log" class="com.xin.log.BeforeLog"/>
<bean id="afterLog" class="com.xin.log.AfterLog"/>
<aop:config>
<!--切入点 execution(返回值 生效方法位置) -->
<aop:pointcut id="userServicePointCut" expression="execution(* com.xin.service.UserServiceImpl.*(..) )"/>
<!--配通知到切入点中,满足切入点表达式的方法会执行-->
<aop:advisor advice-ref="log" pointcut-ref="userServicePointCut"/><!-- 用接口识别生效的时机 -->
<aop:advisor advice-ref="afterLog" pointcut-ref="userServicePointCut"/>
</aop:config>
3、定义一个类@Aspect并注册,在里面的方法上@Pointcut(),@Before(),@After(),@Around()对匹配到的切点进行方法增强。
@Aspect
public class AnnotationPointCut {
@Pointcut("execution(* com.xin.service.UserServiceImpl.*(..)) && args(param)")//携带指定参数,可以对参数进行操作
public void example(int param){} //作示例(模型)
//满足和在com.xin.service.*下都生效
@Before("execution(* com.xin.service.UserServiceImpl.*(..)) && within(com.xin.service.*))")
public void before(){
System.out.println("annotationBefore");
}
@After("example(param)") //能得到切入点的参数进行操作,但是无法修改原始参数
public void after(int param){
param=8;
System.out.println("annotationAfter:"+param);
}
@Around("execution(* com.xin.service.UserServiceImpl.*(..))")
public void around(ProceedingJoinPoint joinPoint) throws Throwable { //joinPoint代表切入点
System.out.println("环绕前");
Object proceed = joinPoint.proceed();
System.out.println("环绕后");
}
}
cglib动态代理,jdk动态代理,AspectJ动态代理
jdk动态代理:实现被代理类接口,方法里反射调原方法进行前后增强。
cglib动态代理:继承被代理类,方法里直接调用原方法进行前后增强。(如果final不能被继承就代理不了)
AspectJ代理:编译时在字节码层面对类方法前后进行增强,需要它的编译器。
Spring事务
事务实现方式之配置通知生效范围
<!--配置事务切入-->
<aop:config>
<aop:pointcut id="txPointCut" expression="execution(* com.xin.mapper.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes> <!--以add开头的-->
<tx:method name="add" propagation="REQUIRED"/> <!--传播特性,默认这个(撒出去,命中就执行事务)-->
<tx:method name="delete" propagation="REQUIRED"/>
<tx:method name="update" propagation="REQUIRED"/>
<tx:method name="query" read-only="true"/>
<tx:method name="*" propagation="REQUIRED"/> <!--所有都要支持事务-->
</tx:attributes>
</tx:advice>
<!--声明式事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
通过配置文件的方式,精确到某方法,对匹配到的所有方法进行事务处理。
事务实现方式之注解
@EnableTransactionManagement后可以使用@Tranceactional注解对方法进行标识,被标识的方法会统一使用Spring委托的本地事务管理方式进行代理执行。
事务实现之原理
1、@EnableTransactionManagement后可以使用@Tranceactional。
2、@EnableTransactionManagement会注册配置类,配置类定义了对@Tranceactional使用通知,创建动态代理。
3、动态代理中事务管理器新建数据库连接,放在ThreadLocal<Map<Datasource,connection>>,设置connection.autocommit=false,会用自己的数据源去ThreadLocal里面当做key去找连接,使用同一个连接统一提交或回滚。
事务实现之失效
事务失效一般有以下几种:
1、方法是非public
2、类没有为bean
3、自己捕获了异常
4、内部调用本类方法导致传播失效
5、抛出受检查异常
6、切面优先级不当
7、父子容器问题
8、方法final,static修饰
9、多线程调用
10、错误的传播行为
11、使用了不支持事务的存储引擎
12、数据源没有配置事务管理器
13、被代理的类过早实例化
其原因为:
1、”spring事务默认生效的方法权限都必须为public
2、”spring事务生效的前提是,service必须是一个bean对象
3、”spring事务只有捕捉到了业务抛出去的异常,才能进行后续的处理,如果业务自己捕获了异常,则事务无法感知
4、“事务方法由代理对象调用控制,内部调用是本普通对象调用
5、“spring默认只会回滚非检查异常和error异常
6、“spring事务切面的优先级顺序最低,但如果自定义的切面优先级和他一样,且自定义的切面没有正确处理异常,则会同业务自己捕获异常的那种场景一样。
7、“子容器扫描范围过大,将未加事务配置的service扫描进来
8、“因为spring事务是用动态代理实现,因此如果方法使用了final,static修饰,则代理类无法对目标方法进行重写,植入事务功能
9、“因为spring的事务是通过数据库连接来实现,而数据库连接spring是放在threadLocal里面。同一个事务,只能用同一个数据库连接。而多线程场景下,拿到的数据库连接是不一样的,即是属于不同事务
10、“使用的传播特性不支持事务,未覆盖到该事务
11、“使用了不支持事务的存储引擎。比如mysql中的MyISAM
12、“SpringBoot一般会自动配置
13、“当代理类的实例化早于AbstractAutoProxyCreator后置处理器,就无法被AbstractAutoProxyCreator后置处理器增强
七大事务传播机制(A调用B)
1,REQUIRED(Spring默认的事务传播类型 required:需要有):
如果A有事务B有事务,则B加入A一起。如果A没有事务B有事务,B自己创建事务执行。
2,SUPPORTS(supports:支持别人):
支持上层做法。A有事务B有事务B就加入A事务执行,A没事务B有事务B就以非事务执行。
3,MANDATORY(mandatory:强制性的):
该事务需要在可回滚事务中进行。A没事务B有事务就报错。
4,REQUIRES_NEW(requires_new:启新事务):
无论上层有没有事务,该事务都会新创建执行。A事务调用B事务,A事务挂起,且各回滚各的。
5,NOT_SUPPORTED(not supported:不支持):
以非事务方式运行,若在事务中调用,则外层事务挂起。
6,NEVER(never:从不):
上下层都不允许有事务,抛异常。
7,NESTED(nested:嵌套的):
A有事务B就嵌套在里面执行,A没事务B就自己创建执行。
REQUIRED和NESTED的区别就是:嵌套时A回滚B也回滚,B回滚A可以捕获异常不回滚。加入时A回滚B也回滚,B回滚不能捕获异常而A也回滚。
Spring注册Bean的几种方式
-
applicationContext.xml中注册,ClassPathXmlApplicationContext来getBean
-
@Configuration的类中方法上@Bean然后return对象注册,AnnotationConfigApplicationContext来getBean
3 在@Configuration上@Import({类.class})注册该类,获取时得getBean(“com.xin.pojo.类”)即全限定名 -
开启包扫描,扫描下的@Component等的类注册,ClassPathXmlApplicationContext来getBean
-
已注册的一个A类实现FactoryBean<B’>接口重写三个方法(getObject,getObjectType,isSingleton)分别return类型
xxxApplicationContext来getBean(“A”)来获得getObject提供的对象也就是B,getBean(“&A”)则是获得A -
在注册的A类实现ImportSelector接口重写方法后return一个String数组,里面放的要注册的类的全限定名
Spring集成jdbc
注册数据源和事务管理器,注册JdbcTemplate使用数据源。
@Configuration
public class JDBCConfig {
@Bean
public JdbcTemplate jdbcTemplate(){
return new JdbcTemplate(dataSource());
}
@Bean
public PlatformTransactionManager transactionManager(){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource());
return dataSourceTransactionManager;
}
@Bean
public DataSource dataSource(){
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setUrl("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8&useUnicode=true&useSSL=false");
driverManagerDataSource.setUsername("root");
driverManagerDataSource.setPassword("123456");
return driverManagerDataSource;
}
}
此后可直接注入JdbcTemplate操作数据库。需要注意的是事务失效原因中的同一数据源才能进行事务。
Spring集成Mybatis
- 在spring-dao.xml中配据源,配SqlSessionFactory,配SqlSessionTemplate,把工具类实现的和mybatis-config.xml提取出来
<!-- spring来管理数据源 需要导spring-jdbc包-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8&useUnicode=true&useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!--绑定mybatis配置文件-->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!--从配置文件里提出来的注册mapper-->
<property name="mapperLocations" value="classpath:com/xin/mapper/*.xml"/>
</bean>
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<!--没有set方法,只能通过构造器注入了-->
<constructor-arg index="0" ref="sqlSessionFactory"/>
</bean>
-
新建UserMapperImpl类去实现增删改查业务,注册这个bean
-
在applicationContext.xml里注册bean,导入spring-dao.xml直接用
Spring-Mybatis日志
在配置文件中配置如下,使用LOG4J,如要使用其他日志,需要导入日志需要导入对应的包。
<settings> <!--SLF4J | LOG4J | LOG4J2| JDK_LOGGING |COMMONS_LOGGING| STDOUT_LOGGING |NO_LOGGING-->
<!-- <setting name="logImpl" value="STDOUT_LOGGING"/>--> <!--标准不用导包-->
<setting name="logImpl" value="LOG4J"/>
</settings>
在log4j.properties中配置日志需要的属性。
log4j.rootLogger=DEBUG,console,LOGFILE
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.Threshold = DEBUG
log4j.appender.console.ImmediateFlush = false
log4j.appender.console.Encoding = UTF-8
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %5p (%c:%L) - %m%n
log4j.appender.LOGFILE=or g.apache.log4j.FileAppender
log4j.appender.LOGFILE.File =./logs/xin.log
log4j.appender.LOGFILE.Append = true
log4j.appender.file.MAXFileSize=10mb
log4j.appender.LOGFILE.Threshold = DEBUG
log4j.appender.LOGFILE.ImmediateFlush = true
log4j.appender.LOGFILE.Encoding = UTF-8
log4j.appender.LOGFILE.layout = org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss}[%t:%r]-[%p]%m%n
Spring依赖注入的三种方式及优缺点
-
在属性上@Autowired等实现注入
优点:简洁明了
缺点:注入对象不能用final属性,可能导致循环依赖。 -
在构造方法上@Autowired等实现注入 (推荐)
优点:强制注入防空指针异常,注入对象可以final,可能出现三级缓存不能解决的循环依赖(生命周期在第一步卡住,第三级缓存无法生效)
缺点:注入过多很不好看 -
在set方法上@Autowired实现注入
优点:注入对象可以为null,可以在类构造后重新注入
缺点:注入对象不能final
Spring三级缓存
三级缓存,是三个map
第一级缓存:singletonObjects 单例池,存放最终单例bean对象
第二级缓存:earlySingletonObjects 存放未完整生命周期的单例bean
第三级缓存:singletonFactories 存放普通对象,出现循环依赖来拿 Map<beanName,()->getEarlyBeanReference(beanName,对应的beanDefinition,普通对象)>
还有一些辅助集合
creatingSet用来存放bean是否在创建中
earlyProxyReference用来存放进行过AOP的代理对象
解决的问题
在开启三级缓存后可以解决一般的循环依赖问题,但在构造方法处使用依赖注入会导致生命周期第一步无法进行,使得三级缓存失效,可以使用懒加载@Lazy进行注入。循环依赖解决的一般流程如下:
private void createBean(){ //A的生命周期
//推断构造方法实例化
// 实例化前把A加到creatingSet中
// 实例化后把A的普通对象加入singletonFactories第三级缓存
//依赖注入
// A依赖B,需要注入B
// B实例化后需要依赖注入A,getSingleton()
// 去第一级缓存单例池里找有没有A,有就注入,没有就看creatingSet中有没有A,没有就创建A的bean对象注入,有A说明出现循环依赖,继续
// 在第二级缓存earlySingletonObjects看是否有A,有就注入,没有继续
// 在第三级缓存singletonFactories里拿到lambda执行,执行如果进行AOP就产生代理对象,代理对象加入earlyProxyReference的map里,用来后面判断是否还要AOP
// 之后把A代理对象(或对象)存入earlySingletonObjects (二级缓存存,三级缓存就删,三级缓存存,二级缓存就删)
// 把第三级缓存里A的lambda销毁(第二级有了以后也不会来第三级来找了,也可以防止bug多例)
// 产生A的代理对象注入B的依赖里
// 然后B在进行一些其他生命周期的操作之后放到单例池并填充进A依赖
//Aware等回调
//初始化前
//初始化
//初始化后
// 尝试删除earlyProxyReference里的这个对象key,删除成功说明进行过了AOP,没有成功看是否需要重新进行AOP
//加到单例池
//拿到某一级缓存里的A代理对象(或对象),此时A普通对象已经经过完整的生命周期了,则A代理对象(或对象)里的普通对象即也经过了,此时已完整,加入单例池
//creatingSet里销毁A
}
此示例为A,B循环依赖大致解决流程。
此外补充:
@Async修饰会产生专门的代理对象,且循环依赖提前AOP时spring不会为其处理产生代理对象。
经过循环依赖后,后面再初始化这个对象时要进行代理,然后会比较经过循环依赖的普通对象和经过这里初始化后的对象是否相等。
不相等报错,也就是经过循环依赖处理后的对象不允许再改变。
相等则把二级缓存的这个对象返回出来。
如果没有循环依赖则不会走这些步骤。