前言
在作業中經常會發現很多同事連最常用的SSM框架使用起來也經常忘這忘那的,關于spring甚至只記得IOC、DI、AOP,然后就在網上找資料浪費大部分時間,所以本文重溫了一遍幫大伙加深理解,同時做個整理,以后再忘來看這篇文章就好了,
我平時也會收集一些不錯的spring學習書籍PDF,畢竟程式員的書都挺貴的,不可能每本都買物體,自己啃完也會梳理學習筆記,都在這了>>spring一網打盡,直接點擊就可以無償獲取,
1. 體系結構

Spring是模塊化的,可以選擇合適的模塊來使用,其體系結構分為5個部分,分別為:
Core Container
核心容器:Spring最主要的模塊,主要提供了IOC、DI、BeanFactory、Context等,列出的這些學習過Spring的同學應該都認識
Data Access/Integration
資料訪問/集成:即有JDBC的抽象層、ORM物件關系映射API、還有事務支持(重要)等
Web
Web:基礎的Web功能如Servlet、http、Web-MVC、Web-Socket等
Test
測驗:支持具有Junit或TestNG框架的Spring組件測驗
其他
AOP、Aspects(面向切面編程框架)等
2. IOC
2.1 引入耦合概念
耦合:即是類間或方法間的依賴關系,編譯時期的依賴會導致后期維護十分困難,一處的改動導致其他依賴的地方都需改動,所以要解耦
解耦:解除程式間的依賴關系,但在實際開發中我們只能做到編譯時期不依賴,運行時期才依賴即可,沒有依賴關系即沒有必要存在了
解決思路:使用Java的反射機制來避免new關鍵字(通過讀取組態檔來獲取物件全限定類名)、使用工廠模式
2.2 IOC容器
Spring框架的核心,主要用來存放Bean物件,其中有個底層BeanFactory介面只提供最簡單的容器功能(特點延遲加載),一般不使用,常用的是其子類介面ApplicationContext介面(創建容器時立即實體化物件,繼承BeanFactory介面),提供了高級功能(訪問資源,決議檔案資訊,載入多個繼承關系的背景關系,攔截器等),
ApplicationContext介面有三個實作類:ClassPathXmlApplicationContext、FileSystemoXmlApplication、AnnotionalConfigApplication,從名字可以知道他們的區別,下面講解都將圍繞ApplicationContext介面,
容器為Map結構,鍵為id,值為Object物件,
2.2.1 Bean的創建方式
無參構造
只配了id、class標簽屬性(此時一定要有無參函式,添加有參構造時記得補回無參構造)
普通工廠創建
可能是別人寫好的類或者jar包,我們無法修改其原始碼(只有位元組碼)來提供無參建構式,eg:
// 這是別人的jar包是使用工廠來獲取實體物件的
public class InstanceFactory {
public User getUser() {
return new User();
}
}
<!-- 工廠類 -->
<bean id="UserFactory" class="com.howl.entity.UserFactory"></bean>
<!-- 指定工廠類及其生產實體物件的方法 -->
<bean id="User" factory-bean="UserFactory" factory-method="getUser"></bean>
靜態工廠創建
<!-- class使用靜態工廠類,方法為靜態方法生產實體物件 -->
<bean id="User" class="com.howl.entity.UserFactory" factory-method="getUser"></bean>
2.2.2 Bean標簽
該標簽在applicationContext.xml中表示一個被管理的Bean物件,Spring讀取xml組態檔后把內容放入Spring的Bean定義注冊表,然后根據該注冊表來實體化Bean物件將其放入Bean快取池中,應用程式使用物件時從快取池中獲取
| 屬性 | 描述 |
|---|---|
| class | 指定用來創建bean類 |
| id | 唯一的識別符號,可用 ID 或 name 屬性來指定 bean 識別符號 |
| scope | 物件的作用域,singleton(默認)/prototype |
| lazy-init | 是否懶創建 true/false |
| init-method | 初始化呼叫的方法 |
| destroy-method | x銷毀呼叫的方法 |
| autowire | 不建議使用,自動裝配byType、byName、constructor |
| factory-bean | 指定工廠類 |
| factory-method | 指定工廠方法 |
| 元素 | 描述 |
| constructor-arg | 建構式注入 |
| properties | 屬性注入 |
| 元素的屬性 | 描述 |
| type | 按照型別注入 |
| index | 按照下標注入 |
| name | 按照名字注入,最常用 |
| value | 給基本型別和String注入 |
| ref | 給其他bean型別注入 |
| 元素的標簽 | 描述 |
<list> | |
<Set> | |
<Map> | |
<props> |
2.2.3 使用
注意:默認使用無參建構式的,若自己寫了有參構造,記得補回無參構造
XML
<bean id="User" class="com.howl.entity.User"></bean>
ApplicationContext ac = new ClassPathXmlApplicationContext
("applicationContext.xml");
User user = (User) ac.getBean("User");
user.getName();
注解
前提在xml組態檔中開啟bean掃描
<context:component-scan base-package="com.howl.entity"></context:component-scan>
// 默認是類名首字母小寫
@Component(value="User")
public class User{
int id;
String name;
String eamil;
}
2.2.4 生命周期
單例:與容器同生共死
多例: 使用時創建,GC回收時死亡
3. DI
Spring框架的核心功能之一就是通過依賴注入的方式來管理Bean之間的依賴關系,能注入的資料型別有三類:基本型別和String,其他Bean型別,集合型別,注入方式有:建構式,set方法,注解
3.1 基于建構式的注入
<!-- 把物件的創建交給Spring管理 -->
<bean id="User" class="com.howl.entity.User">
<constructor-arg type="int" value="1"></constructor-arg>
<constructor-arg index="1" value="Howl"></constructor-arg>
<constructor-arg name="email" value="xxx@qq.com"></constructor-arg>
<constructor-arg name="birthday" ref="brithday"></constructor-arg>
</bean>
<bean id="brithday" class="java.util.Date"></bean>
3.2 基于setter注入(常用)
被注入的bean一定要有setter函式才可注入,而且其不關心屬性叫什么名字,只關心setter叫什么名字
<bean id="User" class="com.howl.entity.User">
<property name="id" value="1"></property>
<property name="name" value="Howl"></property>
<property name="email" value="XXX@qq.com"></property>
<property name="birthday" ref="brithday"></property>
</bean>
<bean id="brithday" class="java.util.Date"></bean>
3.3 注入集合
內部有復雜標簽、、
<bean id="User" class="com.howl.entity.User">
<property name="addressList">
<list>
<value>INDIA</value>
<value>Pakistan</value>
<value>USA</value>
<ref bean="address2"/>
</list>
</property>
<property name="addressSet">
<set>
<value>INDIA</value>
<ref bean="address2"/>
<value>USA</value>
<value>USA</value>
</set>
</property>
<property name="addressMap">
<map>
<entry key="1" value="INDIA"/>
<entry key="2" value-ref="address1"/>
<entry key="3" value="USA"/>
</map>
</property>
<property name="addressProp">
<props>
<prop key="one">INDIA</prop>
<prop key="two">Pakistan</prop>
<prop key="three">USA</prop>
<prop key="four">USA</prop>
</props>
</property>
</bean>
3.4 注解
@Autowired:自動按照型別注入(所以使用注解時setter方法不是必須的,可用在變數上,也可在方法上),若容器中有唯一的一個bean物件型別和要注入的變數型別匹配就可以注入;若一個型別匹配都沒有,則報錯;若有多個型別匹配時:先匹配全部的型別,再繼續匹配id是否有一致的,有則注入,沒有則報錯
@Qualifier:在按照型別注入基礎上按id注入,給類成員變數注入時不能單獨使用,給方法引數注入時可以單獨使用
@Resource:上面二者的結合
注意:以上三個注入只能注入bean型別資料,不能注入基本型別和String,集合型別的注入只能通過XMl方式實作
@Value:注入基本型別和String資料
承接上面有個User類了
@Component(value = "oneUser")
@Scope(value = "singleton")
public class OneUser {
@Autowired // 按型別注入
User user;
@Value(value = "注入的String型別")
String str;
public void UserToString() {
System.out.println(user + str);
}
}
3.5 配置類(在SpringBoot中經常會遇到)
配置類等同于aplicationContext.xml,一般配置類要配置的是需要引數注入的bean物件,不需要引數配置的直接在類上加@Component
/**
* 該類是個配置類,作用與applicationContext.xml相等
* @Configuration表示配置類
* @ComponentScan(value = {""})內容可以傳多個,表示陣列
* @Bean 表示將回傳值放入容器,默認方法名為id
* @Import 匯入其他配置類
* @EnableAspectJAutoProxy 表示開啟注解
*/
@Configuration
@Import(OtherConfiguration.class)
@EnableAspectJAutoProxy
@ComponentScan(value = {"com.howl.entity"})
public class SpringConfiguration {
@Bean(value = "userFactory")
@Scope(value = "prototype")
public UserFactory createUserFactory(){
// 這里的物件容器管理不到,即不能用@Autowired,要自己new出來
User user = new User();
// 這里是基于建構式注入
return new UserFactory(user);
}
}
@Configuration
public class OtherConfiguration {
@Bean("user")
public User createUser(){
User user = new User();
// 這里是基于setter注入
user.setId(1);
user.setName("Howl");
return user;
}
}
4. AOP
4.1 動態代理
動態代理:基于介面(invoke)和基于子類(Enhancer的create方法),基于子類的需要第三方包cglib,這里只說明基于介面的動態代理,筆者 動態代理的博文
Object ob = Proxy.newProxyInstance(mydog.getClass().getClassLoader(), mydog.getClass().getInterfaces(),new InvocationHandler(){
// 引數依次為:被代理類一般不使用、使用的方法、引數的陣列
// 回傳值為創建的代理物件
// 該方法會攔截類的所有方法,并在每個方法內注入invoke內容
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 只增強eat方法
if(method.getName().equals("eat")){
System.out.println("吃肉前洗手");
method.invoke(mydog, args);
}else{
method.invoke(mydog, args);
}
return proxy;
}
})
4.2 AOP
相關術語:
連接點:這里指被攔截的方法(Spring只支持方法)
通知:攔截到連接點要執行的任務
切入點:攔截中要被增強的方法
織入:增強方法的程序
代理物件:增強功能后回傳的物件
切面:整體的結合,什么時候,如何增強方法
xml配置
<!-- 需要額外的jar包,aspectjweaver運算式需要 -->
<!-- 被切入的方法 -->
<bean id="accountServiceImpl" class="com.howl.interfaces.impl.AccountServiceImpl"></bean>
<!-- 通知bean也交給容器管理 -->
<bean id="logger" class="com.howl.util.Logger"></bean>
<!-- 配置aop -->
<aop:config>
<aop:pointcut id="pt1" expression="execution(* com.howl.interfaces..*(..))"/>
<aop:aspect id="logAdvice" ref="logger">
<aop:before method="beforeLog" pointcut-ref="pt1"></aop:before>
<aop:after-returning method="afterReturningLog" pointcut-ref="pt1"></aop:after-returning>
<aop:after-throwing method="afterThrowingLog" pointcut-ref="pt1"></aop:after-throwing>
<aop:after method="afterLog" pointcut="execution(* com.howl.interfaces..*(..))"></aop:after>
<!-- 配置環繞通知,測驗時請把上面四個注釋掉,排除干擾 -->
<aop:around method="aroundLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
<!-- 切入運算式 -->
<!-- 訪問修飾符 . 回傳值 . 包名 . 包名 . 包名,,, . 類名 . 方法名(引數串列) -->
<!-- public void com.howl.Service.UserService.deleteUser() -->
<!-- 訪問修飾符可以省略 -->
<!-- * 表示通配,可用于修飾符,回傳值,包名,方法名 -->
<!-- .. 標志當前包及其子包 -->
<!-- ..可以表示有無引數,*表示有引數 -->
<!-- * com.howl.service.*(..) -->
<!-- 環繞通知是手動編碼方式實作增強方法合適執行的方式,類似于invoke? -->
即環繞通知是手動配置切入方法的,且Spring框架提供了ProceedingJoinPoint,該介面有一個proceed()和getArgs()方法,此方法就明確相當于呼叫切入點方法和獲取引數,在程式執行時,spring框架會為我們提供該介面的實作類供我們使用
// 抽取了公共的代碼(日志)
public class Logger {
public void beforeLog(){
System.out.println("前置通知");
}
public void afterReturningLog(){
System.out.println("后置通知");
}
public void afterThrowingLog(){
System.out.println("例外通知");
}
public void afterLog(){
System.out.println("最終通知");
}
// 這里就是環繞通知
public Object aroundLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
// 獲取方法引數
Object[] args = pjp.getArgs();
System.out.println("前置通知");
// 呼叫業務層方法
rtValue = pjp.proceed();
System.out.println("后置通知");
} catch (Throwable t) {
System.out.println("例外通知");
t.printStackTrace();
} finally {
System.out.println("最終通知");
}
return rtValue;
}
}
基于注解的AOP
<!-- 配置Spring創建容器時要掃描的包,主要掃描被切入的類,以及切面類 -->
<context:compinent-scan base-package="com.howl.*"></context:compinent-scan>
<!-- 這二者的類上要注解 @Compinent / @Service -->
<!-- 開啟AOP注解支持 -->
<aop:aspectj:autoproxy></aop:aspectj:autoproxy>>
注意要在切面類上加上注解表示是個切面類,四個通知在注解中通知順序是不能決定的且亂序,不建議使用,不過可用環繞通知代替 ,即注解中建議使用環繞通知來代替其他四個通知
// 抽取了公共的日志
@Component(value = "logger")
@Aspect
public class Logger {
@Pointcut("execution(* com.howl.interfaces..*(..))")
private void pt1(){}
@Before("pt1()")
public void beforeLog(){
System.out.println("前置通知");
}
@AfterReturning("pt1()")
public void afterReturningLog(){
System.out.println("后置通知");
}
@AfterThrowing("pt1()")
public void afterThrowingLog(){
System.out.println("例外通知");
}
@After("pt1()")
public void afterLog(){
System.out.println("最終通知");
}
@Around("pt1()")
public Object aroundLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
// 獲取方法引數
Object[] args = pjp.getArgs();
System.out.println("前置通知");
// 呼叫業務層方法
rtValue = pjp.proceed();
System.out.println("后置通知");
} catch (Throwable t) {
System.out.println("例外通知");
t.printStackTrace();
} finally {
System.out.println("最終通知");
}
return rtValue;
}
}
5. 事務
Spring提供了宣告式事務和編程式事務,后者難于使用而選擇放棄,Spring提供的事務在業務層,是基于AOP的
5.1 宣告式事務
從業務代碼中分離事務管理,僅僅使用注釋或 XML 配置來管理事務,Spring 把事務抽象成介面 org.springframework.transaction.PlatformTransactionManager ,其內容如下,重要的是其只是個介面,真正實作類是:org.springframework.jdbc.datasource.DataSourceTransactionManager
public interface PlatformTransactionManager {
// 根據定義創建或獲取當前事務
TransactionStatus getTransaction(TransactionDefinition definition);
void commit(TransactionStatus status);
void rollback(TransactionStatus status);
}
TransactionDefinition事務定義資訊
public interface TransactionDefinition {
int getPropagationBehavior();
int getIsolationLevel();
String getName();
int getTimeout();
boolean isReadOnly();
}
因為不熟悉所以把程序全部貼下來
5.2 xml配置
建表
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`money` int(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
entity
public class Account {
private int id;
private int money;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
public Account(int id, int money) {
this.id = id;
this.money = money;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", money=" + money +
'}';
}
}
Dao層
public interface AccountDao {
// 查找賬戶
public Account selectAccountById(int id);
// 更新賬戶
public void updateAccountById(@Param(value = "id") int id, @Param(value = "money") int money);
}
Mapper層
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.howl.dao.AccountDao">
<select id="selectAccountById" resultType="com.howl.entity.Account">
SELECT * FROM account WHERE id = #{id};
</select>
<update id="updateAccountById">
UPDATE account SET money = #{money} WHERE id = #{id}
</update>
</mapper>
Service層
public interface AccountService {
public Account selectAccountById(int id);
public void transfer(int fid,int sid,int money);
}
Service層Impl
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
public Account selectAccountById(int id) {
return accountDao.selectAccountById(id);
}
// 這里只考慮事務,不關心錢額是否充足
public void transfer(int fid, int sid, int money) {
Account sourceAccount = accountDao.selectAccountById(fid);
Account targetAccount = accountDao.selectAccountById(sid);
accountDao.updateAccountById(fid, sourceAccount.getMoney() - money);
// 例外
int i = 1 / 0;
accountDao.updateAccountById(sid, targetAccount.getMoney() + money);
}
}
applicationContext.xml配置
<!-- 配置資料源,spring自帶的沒有連接池功能 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/spring"></property>
<property name="username" value="root"></property>
<property name="password" value=""></property>
</bean>
<!-- 配置sqlSessionFactory工廠 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="configLocation" value="classpath:mybatis-config.xml"></property>
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 業務層bean -->
<bean id="accountServiceImpl" class="com.howl.service.impl.AccountServiceImpl" lazy-init="true">
<property name="sqlSessionFactory" ref="sqlSessionFactory"></property>
</bean>
<!-- 事務管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事務通知,可以理解為Logger -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 配置事務的屬性
isolation:隔離界別,默認使用資料庫的
propagation:轉播行為,默認REQUIRED
read-only:只有查詢方法才需要設定true
timeout:默認-1永不超時
no-rollback-for
rollback-for
-->
<tx:attributes>
<!-- name中是選擇匹配的方法 -->
<tx:method name="select*" propagation="SUPPORTS" read-only="true"></tx:method>
<tx:method name="*" propagation="REQUIRED" read-only="false"></tx:method>
</tx:attributes>
</tx:advice>
<!-- 配置AOP -->
<aop:config>
<aop:pointcut id="pt1" expression="execution(* com.howl.service.impl.AccountServiceImpl.transfer(..))"/>
<!-- 建立切入點運算式與事務通知的對應關系 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor>
</aop:config>
測驗
public class UI {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
AccountService accountService = (AccountService) ac.getBean("accountServiceImpl");
Account account = accountService.selectAccountById(1);
System.out.println(account);
accountService.transfer(1,2,100);
}
}
正常或發生例外都完美運行
個人覺得重點在于配置事務管理器(而像資料源這樣是日常需要)
事務管理器:管理獲取的資料庫連接
事務通知:根據事務管理器來配置所需要的通知(類似于前后置通知)
上面兩個可以認為是合一起配一個通知,而下面的配置方法與通知的映射關系
AOP配置:用特有的<aop:advisor>標簽來說明這是一個事務,需要在哪些地方切入
5.3 注解事務
- 配置事務管理器(和xml一樣必須的)
- 開啟Spring事務注解支持
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven> - 在需要注解的地方使用@Transaction
- 不需要AOP,是因為@Transaction注解放在了哪個類上就說明哪個類需要切入,里面所有方法都是切入點,映射關系已經存在了
在AccountServiceImpl中簡化成,xml中可以選擇方法匹配,注解不可,只能這樣配
@Service(value = "accountServiceImpl")
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public class AccountServiceImpl implements AccountService {
// 這里為了獲取Dao層
@Autowired
private AccountDao accountDao;
// 業務正式開始
public Account selectAccountById(int id) {
return accountDao.selectAccountById(id);
}
// 這里只考慮事務,不關心錢額是否充足
@Transactional(propagation = Propagation.REQUIRED,readOnly = false)
public void transfer(int fid, int sid, int money) {
Account sourceAccount = accountDao.selectAccountById(fid);
Account targetAccount = accountDao.selectAccountById(sid);
accountDao.updateAccountById(fid, sourceAccount.getMoney() - money);
// 例外
// int i = 1 / 0;
accountDao.updateAccountById(sid, targetAccount.getMoney() + money);
}
}
6. Test
應用程式的入口是main方法,而JUnit單元測驗中,沒有main方法也能執行,因為其內部集成了一個main方法,該方法會自動判斷當前測驗類哪些方法有@Test注解,有就執行,
JUnit不會知道我們是否用了Spring框架,所以在執行測驗方法時,不會為我們讀取Spring的組態檔來創建核心容器,所以不能使用@Autowired來注入依賴,
解決方法:
- 匯入JUnit包
- 匯入Spring整合JUnit的包
- 替換Running,@RunWith(SpringJUnit4ClassRunner.class)
- 加入組態檔,@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)
//@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class UITest {
@Autowired
UserFactory userFactory;
@Test
public void User(){
System.out.println(userFactory.getUser().toString());
}
}
7. 注解總覽
@Component
@Controller
@Service
@Repository
@Autowired
@Qualifier
@Resource
@Value
@Scope
@Configuration
@ComponentScan
@Bean
@Import
@PropertySource()
@RunWith
@ContextConfiguration
@Transactional
8. 總結
學完Spring之后感覺有什么優勢呢?
-
IOC、DI:方便降耦
-
AOP:重復的功能形成組件,在需要處切入,切入出只需關心自身業務甚至不知道有組件切入,也可把切入的組件放到開發的最后才完成
-
宣告式事務的支持
-
最小侵入性:不用繼承或實作他們的類和介面,沒有系結了編程,Spring盡可能不讓自身API弄亂開發者代碼
-
整合測驗
-
方便集成其他框架
好了,本文就寫到這里吧,我的學習秘籍都在這了>>spring一網打盡,直接點擊就可以直接白嫖了!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/291182.html
標籤:其他
下一篇:【程式人生】階段總結-翰墨流離
