当前位置: 首页 > news >正文

Spring AOP与事务

目录

1.AOP简介

1.1什么是AOP

1.2AOP作用

1.3AOP核心概念

2.AOP入门案例

2.1需求分析

2.2思路分析

2.3环境准备

2.4AOP实现步骤

3.AOP工作流程

3.1AOP工作流程

流程1:Spring容器启动

流程2:读取所有切面配置中的切入点

流程3:初始化bean

流程4:获取bean执行方法

3.2AOP核心概念

4.AOP配置管理

4.1AOP切入点表达式

4.1.1语法格式

4.1.2通配符

 4.1.3书写技巧

4.2AOP通知类型

4.2.1类型介绍

4.3业务层接口执行效率

4.3.1需求分析

4.3.2环境准备

4.3.3功能开发

4.4AOP通知获取数据

4.4.1获取参数

4.4.2获取返回值

 4.4.3获取异常

5.AOP总结

5.1 AOP的核心概念

5.2 切入点表达式

5.3 五种通知类型

5.4 通知中获取参数

6.AOP事务管理

6.1Spring事务简介

6.1.1相关概念介绍

6.1.3转账案例-环境搭建

6.1.4事务管理

6.2 Spring事务角色

6.3 Spring事务属性


1.AOP简介

AOP 是在不改原有代码的前提下对其进行增强。

1.1什么是AOP

AOP(Aspect Oriented Programming) 面向切面编程,一种编程范式,指导开发者如何组织程
序结构。 AOP 是一种编程思想,编程思想主要的内容就是指导程序员该 如何编写程序。

1.2AOP作用

作用 : 在不惊动原始设计的基础上为其进行功能增强,前面咱们有技术就可以实现这样的功能即代
理模式。

1.3AOP核心概念

为了能更好的理解 AOP 的相关概念,我们准备了一个环境,整个环境的内容我们暂时可以不用关
注,最主要的类为 : BookDaoImpl

@Repository
public class BookDaoImpl implements BookDao {

    public void save() {
        //记录程序当前执行执行(开始时间)
        Long startTime = System.currentTimeMillis();
        //业务执行万次
        for (int i = 0;i<10000;i++) {
            System.out.println("book dao save ...");
        }
        //记录程序当前执行时间(结束时间)
        Long endTime = System.currentTimeMillis();
        //计算时间差
        Long totalTime = endTime-startTime;
        //输出信息
        System.out.println("执行万次消耗时间:" + totalTime + "ms");
    }

    public void update(){
        System.out.println("book dao update ...");
    }

    public void delete(){
        System.out.println("book dao delete ...");
    }

    public void select(){
        System.out.println("book dao select ...");
    }
}
当在 App 类中从容器中获取 bookDao 对象后,分别执行其 save , delete , update select 方法后会
有如下的打印结果 :
对于计算万次执行消耗的时间只有 save 方法有,为什么 delete update 方法也会有呢 ?
delete update 方法有,那什么 select 方法为什么又没有呢 ?
这个案例中其实就使用了 Spring AOP ,在不惊动 ( 改动 ) 原有设计 ( 代码 ) 的前提下,想给谁添
加功 就给谁添加。这个也就是 Spring 的理念:

AOP实现原理图:

(1) 前面一直在强调, Spring AOP 是对一个类的方法在不进行任何修改的前提下实现增强。对于
上面的案例中 BookServiceImpl 中有 save , update , delete select 方法 , 这些方法我们给起了一
个名字叫 连接点
(2) BookServiceImpl 的四个方法中, update delete 只有打印没有计算万次执行消耗时间,
但是在运行的时候已经有该功能,那也就是说 update delete 方法都已经被增强,所以对于需要增
强的方法我们给起了一个名字叫 切入点
(3) 执行 BookServiceImpl update delete 方法的时候都被添加了一个计算万次执行消耗时间
的功能,将这个功能抽取到一个方法中,换句话说就是存放共性功能的方法,我们给起了个名字叫
(4) 通知是要增强的内容,会有多个,切入点是需要被增强的方法,也会有多个,那哪个切入点需
要添 加哪个通知,就需要提前将它们之间的关系描述清楚,那么对于通知和切入点之间的关系描
述,我们 给起了个名字叫 切面
(5) 通知是一个方法,方法不能独立存在需要被写在一个类中,这个类我们也给起了个名字叫 通知
至此 AOP 中的核心概念就已经介绍完了,总结下 :
连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
SpringAOP 中,理解为方法的执行
切入点(Pointcut):匹配连接点的式子
SpringAOP 中,一个切入点可以描述一个具体方法,也可也匹配多个方法
一个具体的方法 : com.itheima.dao 包下的 BookDao 接口中的无形参无返回值的 save
匹配多个方法 : 所有的 save 方法,所有的 get 开头的方法,所有以 Dao 结尾的接口中的任意
方法,所有带有一个参数的方法
连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一
定要被增强,所以可能不是切入点。
通知(Advice):在切入点处执行的操作,也就是共性功能
SpringAOP 中,功能最终以方法的形式呈现

2.AOP入门案例

2.1需求分析

总结需求为 : 使用 SpringAOP 的注解方式完成在方法执行的前打印出当前系统时间。

2.2思路分析

1. 导入坐标 (pom.xml)
2. 制作连接点 ( 原始操作, Dao 接口与实现类 )
3. 制作共性功能 ( 通知类与通知 )
4. 定义切入点
5. 绑定切入点与通知关系 ( 切面 )

2.3环境准备

创建一个Maven项目

pom.xml添加Spring依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
    </dependencies>
添加 BookDao BookDaoImpl
package com.itheima.dao;

public interface BookDao {

    public void save();
    public void update();
}
package com.itheima.dao.impl;

import com.itheima.dao.BookDao;
import org.springframework.stereotype.Repository;

@Repository
public class BookDaoImpl implements BookDao {
    @Override
    public void save() {
        System.out.println(System.currentTimeMillis());
        System.out.println("book dao save ...");
    }

    @Override
    public void update() {
        System.out.println(System.currentTimeMillis());
        System.out.println("book dao update ...");
    }
}
创建 Spring 的配置类
package com.itheima.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.itheima")
public class SpringConfig {
}
编写 App 运行类
package com.itheima;

import com.itheima.config.SpringConfig;
import com.itheima.dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class App {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = applicationContext.getBean(BookDao.class);
        bookDao.save();
    }
}
最终创建好的项目结构如下 :

说明 :
目前打印 save 方法的时候,因为方法中有打印系统时间,所以运行的时候是可以看到系统时间
对于 update 方法来说,就没有该功能
我们要使用 SpringAOP 的方式在不改变 update 方法的前提下让其具有打印系统时间的功能。

2.4AOP实现步骤

步骤 1: 添加依赖
pom.xml
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
      <version>1.9.4</version>
    </dependency>

因为spring-context中已经导入了spring-aop ,所以不需要再单独导入spring-aop

导入 AspectJ jar ,AspectJ AOP 思想的一个具体实现, Spring 有自己的 AOP 实现,但是相
比于 AspectJ 来说比较麻烦,所以我们直接采用 Spring 整合 ApsectJ 的方式进行 AOP 开发。

步骤 3: 定义通知类和通知
public class MyAdvice {
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}
步骤 4: 定义切入点
BookDaoImpl 中有两个方法,分别是 save update ,我们要增强的是 update 方法,该如何定义呢 ?
public class MyAdvice {
    //设置切入点,要求配置在方法上方
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}

    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}
步骤 5: 制作切面
切面是用来描述通知和切入点之间的关系,如何进行关系的绑定 ?
绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行 位置
public class MyAdvice {
    //设置切入点,要求配置在方法上方
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}

    //设置在切入点pt()的前面运行当前操作(前置通知)
    // @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

步骤 6: 将通知类配给容器并标识其为切面类
@Component
//设置当前类为切面类类
@Aspect
public class MyAdvice {
    //设置切入点,要求配置在方法上方
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}

    //设置在切入点pt()的前面运行当前操作(前置通知)
    // @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}
步骤 7: 开启注解格式 AOP 功能
@Configuration
@ComponentScan("com.itheima")
//开启注解开发AOP功能
@EnableAspectJAutoProxy
public class SpringConfig {
}
步骤 8: 运行程序
public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        bookDao.update();
    }
}
看到在执行 update 方法之前打印了系统时间戳,说明对原始方法进行了增强, AOP 编程成功。

3.AOP工作流程

知识点 : AOP 工作流程 AOP 核心概念

3.1AOP工作流程

由于 AOP 是基于 Spring 容器管理的 bean 做的增强,所以整个工作过程需要从 Spring 加载 bean 说起 :

流程1:Spring容器启动

容器启动就需要去加载 bean, 哪些类需要被加载呢 ?
需要被增强的类,如 :BookServiceImpl
通知类,如 :MyAdvice
注意此时 bean 对象还没有创建成功

流程2:读取所有切面配置中的切入点

 上面这个例子中有两个切入点的配置,但是第一个ptx()并没有被使用,所以不会被读取。

流程3:初始化bean

判定bean对应的类中的方法是否匹配到任意切入点
注意第 1 步在容器启动的时候, bean 对象还没有被创建成功。
要被实例化 bean 对象的类中的方法和切入点进行匹配

匹配失败,创建原始对象,UserDao
匹配失败说明不需要增强,直接调用原始对象的方法即可。
匹配成功,创建原始对象(目标对象)的代理对象,: BookDao
匹配成功说明需要对其进行增强
对哪个类做增强,这个类对应的对象就叫做目标对象
因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象
最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强

流程4:获取bean执行方法

获取的 bean 是原始对象时,调用方法并执行,完成操作
获取的 bean 是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
验证容器中是否为代理对象
为了验证 IOC 容器中创建的对象和我们刚才所说的结论是否一致,首先先把结论理出来 :
如果目标对象中的方法会被增强,那么容器中将存入的是目标对象的代理对象
如果目标对象中的方法不被增强,那么容器中将存入的是目标对象本身。
验证思路

1. 要执行的方法,不被定义的切入点包含,即不要增强,打印当前类的 getClass() 方法
2. 要执行的方法,被定义的切入点包含,即要增强,打印出当前类的 getClass() 方法
3. 观察两次打印的结果

 步骤1:修改App,获取类的类型

public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
//        bookDao.update();
        System.out.println(bookDao);
        System.out.println(bookDao.getClass());
    }
}
步骤 2: 修改 MyAdvice 类,不增强
因为定义的切入点中,被修改成 update1 , 所以 BookDao 中的 update 方法在执行的时候,就不会被
增强, 所以容器中的对象应该是目标对象本身。
@Component
//设置当前类为切面类类
@Aspect
public class MyAdvice {
    //设置切入点,要求配置在方法上方
    @Pointcut("execution(void com.itheima.dao.BookDao.update1())")
    private void pt(){}

    //设置在切入点pt()的前面运行当前操作(前置通知)
    // @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
步骤 3: 运行程序
步骤 4: 修改 MyAdvice 类,增强
因为定义的切入点中,被修改成 update , 所以 BookDao 中的 update 方法在执行的时候,就会被增
强,所以容器中的对象应该是目标对象的代理对象
@Component
//设置当前类为切面类类
@Aspect
public class MyAdvice {
    //设置切入点,要求配置在方法上方
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}

    //设置在切入点pt()的前面运行当前操作(前置通知)
    // @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
步骤 5: 运行程序

至此对于刚才的结论,我们就得到了验证,这块大家需要注意的是 :
不能直接打印对象,从上面两次结果中可以看出,直接打印对象走的是对象的toString方法,不管
是不是代理对象打印的结果都是一样的,原因是内部对toString方法进行了重写。

3.2AOP核心概念

目标对象(Target) 原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终
工作的
代理(Proxy) 目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实
上面这两个概念比较抽象,简单来说,
目标对象就是要增强的类 [ :BookServiceImpl ] 对应的对象,也叫原始对象,不能说它不能运
行,只能说它在运行的过程中对于要增强的内容是缺失的。
SpringAOP 是在不改变原有设计 ( 代码 ) 的前提下对其进行增强的,它的底层采用的是代理模式实现
的,所以要对原始对象进行增强,就需要对原始对象创建代理对象,在代理对象中的方法把通知
[ :MyAdvice 中的 method 方法 ] 内容加进去,就实现了增强 , 这就是我们所说的代理 (Proxy)
AOP 的核心概念
目标对象、连接点、切入点
通知类、通知
切面
代理
SpringAOP 的本质或者可以说底层实现是通过代理模式。

4.AOP配置管理

4.1AOP切入点表达式

语法格式 通配符 书写技巧

4.1.1语法格式

切入点 : 要进行增强的方法
切入点表达式 : 要进行增强的方法的描述方式
对于切入点的描述,我们其实是有两中方式的,先来看下前面的例子

对于切入点表达式的语法为:

切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常
名)
execution(public User com.itheima.service.UserService.findById(int))
execution :动作关键字,描述切入点的行为动作,例如 execution 表示执行到指定切入点
public: 访问修饰符 , 还可以是 public private 等,可以省略
User :返回值,写返回值类型
com.itheima.service :包名,多级包使用点连接
UserService: / 接口名称
findById :方法名
int: 参数,直接写参数的类型,多个类型用逗号隔开
异常名:方法定义中抛出指定异常,可以省略

4.1.2通配符

 4.1.3书写技巧

所有代码按照标准规范开发,否则以下技巧全部失效
描述切入点通 常描述接口 ,而不描述实现类 , 如果描述到实现类,就出现紧耦合了
访问控制修饰符针对接口开发均采用 public 描述( 可省略访问控制修饰符描述
返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用 * 通配快速描述
包名 书写 尽量不使用 .. 匹配 ,效率过低,常用 * 做单个包描述匹配,或精准匹配
接口名 / 类名 书写名称与模块相关的 采用 * 匹配 ,例如 UserService 书写成 *Service ,绑定业务
层接口名
方法名 书写以 动词 进行 精准匹配 ,名词采用 匹配,例如 getById 书写成 getBy ,selectAll 书写成
selectAll
参数规则较为复杂,根据业务方法灵活调整
通常 不使用异常 作为 匹配 规则

4.2AOP通知类型

4.2.1类型介绍

共提供了 5 种通知类型 :
前置通知
后置通知
环绕通知 ( 重点 )
返回后通知 ( 了解 )
抛出异常后通知 ( 了解 )
知识点 1 @After

知识点 2 @AfterReturning

 知识点3@AfterThrowing

 知识点4@Around

环绕通知注意事项
1. 环绕通知必须依赖形参 ProceedingJoinPoint 才能实现对原始方法的调用,进而实现原始方法
调用前后同时添加通知
2. 通知中如果未使用 ProceedingJoinPoint 对原始方法进行调用将跳过原始方法的执行
3. 对原始方 法的调用可以不接收返回值,通知方法设置成 void 即可,如果接收返回值,最好
设定为 Object 类型
4. 原始方法的返回值如果是 void 类型,通知方法的返回值类型可以设置成 void, 也可以设置成
Object
5. 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理 Throwable 异常

4.3业务层接口执行效率

4.3.1需求分析

需求 : 任意业务层接口执行均可显示其执行效率(执行时长)

具体实现的思路 :
(1) 开始执行方法之前记录一个时间
(2) 执行方法
(3) 执行完方法之后记录一个时间
(4) 用后一个时间减去前一个时间的差值,就是我们需要的结果

4.3.2环境准备

创建一个 Maven 项目
pom.xml 添加 Spring 依赖
  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>

    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
      <version>1.9.4</version>
    </dependency>

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.47</version>
    </dependency>

    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.1.16</version>
    </dependency>

    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.6</version>
    </dependency>

    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.3.0</version>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
添加 AccountService AccountServiceImpl AccountDao Account
package com.itheima.service;

import com.itheima.domain.Account;

import java.util.List;

public interface AccountService {

    void save(Account account);

    void delete(Integer id);

    void update(Account account);

    List<Account> findAll();

    Account findById(Integer id);

}

package com.itheima.service.impl;

import com.itheima.dao.AccountDao;
import com.itheima.domain.Account;
import com.itheima.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    public void save(Account account) {
        accountDao.save(account);
    }

    public void update(Account account){
        accountDao.update(account);
    }

    public void delete(Integer id) {
        accountDao.delete(id);
    }

    public Account findById(Integer id) {
        return accountDao.findById(id);
    }

    public List<Account> findAll() {
        return accountDao.findAll();
    }
}

package com.itheima.domain;

import java.io.Serializable;

public class Account implements Serializable {

    private Integer id;
    private String name;
    private Double money;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Double getMoney() {
        return money;
    }

    public void setMoney(Double money) {
        this.money = money;
    }

    @Override
    public String toString() {
        return "Account{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", money=" + money +
                '}';
    }
}

package com.itheima.dao;

import com.itheima.domain.Account;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import java.util.List;

public interface AccountDao {

    @Insert("insert into tbl_account(name,money)values(#{name},#{money})")
    void save(Account account);

    @Delete("delete from tbl_account where id = #{id} ")
    void delete(Integer id);

    @Update("update tbl_account set name = #{name} , money = #{money} where id = #{id} ")
    void update(Account account);

    @Select("select * from tbl_account")
    List<Account> findAll();

    @Select("select * from tbl_account where id = #{id} ")
    Account findById(Integer id);
}
resources 下提供一个 jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=root
创建相关配置类
package com.itheima.config;

import org.springframework.context.annotation.*;

@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
public class SpringConfig {
}


package com.itheima.config;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;

public class MybatisConfig {

    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        ssfb.setTypeAliasesPackage("com.itheima.domain");
        ssfb.setDataSource(dataSource);
        return ssfb;
    }

    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage("com.itheima.dao");
        return msc;
    }
}


package com.itheima.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;


public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String userName;
    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource dataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(userName);
        ds.setPassword(password);
        return ds;
    }
}
编写 Spring 整合 Junit 的测试类
package com.itheima.service;

import com.itheima.config.SpringConfig;
import com.itheima.domain.Account;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.List;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTestCase {
    @Autowired
    private AccountService accountService;

    @Test
    public void testFindById(){
        Account ac = accountService.findById(2);
    }

    @Test
    public void testFindAll(){
        List<Account> all = accountService.findAll();
    }

}
最终创建好的项目结构如下 :

4.3.3功能开发

步骤 1: 开启 SpringAOP 的注解功能
Spring 的主配置文件 SpringConfig 类中添加注解
@EnableAspectJAutoProxy
步骤 2: 创建 AOP 的通知类
该类要被 Spring 管理,需要添加 @Component
要标识该类是一个 AOP 的切面类,需要添加 @Aspect
配置切入点表达式,需要添加一个方法,并添加 @Pointcut
@Component
@Aspect
public class ProjectAdvice {
    //匹配业务层的所有方法
    @Pointcut("execution(* com.itheima.service.*Service.*(..))")
    private void servicePt(){}
    
    public void runSpeed(){
    }
}
步骤 3: 添加环绕通知
runSpeed() 方法上添加 @Around
@Component
@Aspect
public class ProjectAdvice {
    //匹配业务层的所有方法
    @Pointcut("execution(* com.itheima.service.*Service.*(..))")
    private void servicePt(){}
    //@Around("ProjectAdvice.servicePt()") 可以简写为下面的方式
    @Around("servicePt()")
    public void runSpeed(){
    }
}
注意 : 目前并没有做任何增强
步骤 4: 完成核心业务,记录万次执行的时间
@Component
@Aspect
public class ProjectAdvice {
    //匹配业务层的所有方法
    @Pointcut("execution(* com.itheima.service.*Service.*(..))")
    private void servicePt(){}
    //@Around("ProjectAdvice.servicePt()") 可以简写为下面的方式
    @Around("servicePt()")
    public void runSpeed(ProceedingJoinPoint pjp){
        long start = System.currentTimeMillis();
        for(int i = 0; i < 100; i++){
            pjp.proceed();
         }
        long end = System.currentTimeMillis();
        System.out.println("业务层接口万次执行时间: "+(end-start)+"ms");
    }
}
步骤 5: 运行单元测试类

 步骤6:程序优化

目前程序所面临的问题是,多个方法一起执行测试的时候,控制台都打印的是 :
业务层接口万次执行时间 :xxxms
我们没有办法区分到底是哪个接口的哪个方法执行的具体时间,具体如何优化 ?
@Component
@Aspect
public class ProjectAdvice {
    //匹配业务层的所有方法
    @Pointcut("execution(* com.itheima.service.*Service.*(..))")
    private void servicePt(){}

    //设置环绕通知,在原始操作的运行前后记录执行时间
    @Around("ProjectAdvice.servicePt()")
    public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
        //获取执行的签名对象
        Signature signature = pjp.getSignature();
        String className = signature.getDeclaringTypeName();
        String methodName = signature.getName();

        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
           pjp.proceed();
        }
        long end = System.currentTimeMillis();
        System.out.println("万次执行:"+ className+"."+methodName+"---->" +(end-start) + "ms");
    }

}
步骤 7: 运行单元测试类

4.4AOP通知获取数据

获取切入点方法的参数,所有的通知类型都可以获取参数
JoinPoint 适用于前置、后置、返回后、抛出异常后通知
ProceedingJoinPoint 适用于环绕通知
获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究
返回后通知
环绕通知
获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究
抛出异常后通知
环绕通知

4.4.1获取参数

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
    private void pt(){}

    //JoinPoint:用于描述切入点的对象,必须配置成通知方法中的第一个参数,可用于获取原始方法调用的参数
//    @Before("pt()")
    public void before(JoinPoint jp) {
        Object[] args = jp.getArgs();
        System.out.println(Arrays.toString(args));
        System.out.println("before advice ..." );
    }

//    @After("pt()")
    public void after(JoinPoint jp) {
        Object[] args = jp.getArgs();
        System.out.println(Arrays.toString(args));
        System.out.println("after advice ...");
    }

    //ProceedingJoinPoint:专用于环绕通知,是JoinPoint子类,可以实现对原始方法的调用
//    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) {
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        args[0] = 666;
        Object ret = null;
        try {
            ret = pjp.proceed(args);
        } catch (Throwable t) {
            t.printStackTrace();
        }
        return ret;
    }

    //设置返回后通知获取原始方法的返回值,要求returning属性值必须与方法形参名相同
    @AfterReturning(value = "pt()",returning = "ret")
    public void afterReturning(JoinPoint jp,String ret) {
        System.out.println("afterReturning advice ..."+ret);
    }

    //设置抛出异常后通知获取原始方法运行时抛出的异常对象,要求throwing属性值必须与方法形参名相同
    @AfterThrowing(value = "pt()",throwing = "t")
    public void afterThrowing(Throwable t) {
        System.out.println("afterThrowing advice ..."+t);
    }
}
public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        String name = bookDao.findName(100,"itheima");
        System.out.println(name);
    }
}

 运行App类,可以获取如下内容,说明参数100已经被获取

 使用JoinPoint的方式获取参数适用于前置后置返回后抛出异常后通知。

环绕通知获取方式

环绕通知使用的是 ProceedingJoinPoint ,因为 ProceedingJoinPoint JoinPoint 类的子
类,所以对于 ProceedingJoinPoint 类中应该也会有对应的 getArgs() 方法
注意 :
pjp.proceed() 方法是有两个构造方法,分别是 :
调用无参数的 proceed ,当原始方法有参数,会在调用的过程中自动传入参数
所以调用这两个方法的任意一个都可以完成功能
但是当需要修改原始方法的参数时,就只能采用带有参数的方法 , 如下 :
//    ProceedingJoinPoint:专用于环绕通知,是JoinPoint子类,可以实现对原始方法的调用
    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) {
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        args[0] = 666;
        Object ret = null;
        try {
            ret = pjp.proceed(args);
        } catch (Throwable t) {
            t.printStackTrace();
        }
        return ret;
    }

4.4.2获取返回值

对于返回值,只有返回后AfterReturing和环绕Around 这两个通知类型可以获取,具体如何获取 ?
对于@Around使用pjp.proceed()可以获得原始方法的返回值,并且我们可以进行修改
对于@AfterReturning,需要在注解处进行声明,retuning="ret",目标方法参数为ret。
注意 :
(1) 参数名的问题
(2)afterReturning 方法参数类型的问题
参数类型可以写成 String ,但是为了能匹配更多的参数类型,建议写成 Object 类型
(3)afterReturning 方法参数的顺序问题

 4.4.3获取异常

在AOP中的 catch 方法中就可以获取到异常,至于获取到异常以后该如何处理,这个就和你的业务
需求有关 了。

注意:

在原始方法中抛出异常,在目标方法中注解使用throwing="t",对异常进行接收

运行 App 后,查看控制台,就能看的异常信息被打印到控制台

5.AOP总结

5.1 AOP的核心概念

概念: AOP(Aspect Oriented Programming) 面向切面编程,一种编程范式
作用:在不惊动原始设计的基础上为方法进行功能 增强
核心概念
代理( Proxy ): SpringAOP 的核心本质是采用代理模式实现的
连接点( JoinPoint ):在 SpringAOP 中,理解为任意方法的执行
切入点( Pointcut ):匹配连接点的式子,也是具有共性功能的方法描述
通知( Advice ):若干个方法的共性功能,在切入点处执行,最终体现为一个方法
切面( Aspect ):描述通知与切入点的对应关系
目标对象( Target ):被代理的原始对象成为目标对象

5.2 切入点表达式

切入点表达式标准格式:动作关键字 ( 访问修饰符 返回值 包名 . / 接口名 . 方法名(参数)异常
名) execution(* com.itheima.service.*Service.*(..))
切入点表达式描述通配符:
作用:用于快速描述,范围描述
* :匹配任意符号(常用)
.. :匹配多个连续的任意符号(常用)
+ :匹配子类类型
切入点表达式书写技巧
1. 标准规范 开发 2. 查询操作的返回值建议使用 * 匹配 3. 减少使用 .. 的形式描述包 4. 对接口
进行描述 ,使用 * 表示模块名,例如 UserService 的匹配描述为 *Service 5. 方法名书写保留动
词,例如 get ,使用 * 表示名词,例如 getById 匹配描述为 getBy* 6. 参数根据实际情况灵活调整

5.3 五种通知类型

前置通知
后置通知
环绕通知(重点)
        环绕通知依赖形参ProceedingJoinPoint 才能实现对原始方法的调用
        环绕通知可以隔离原始方法的调用执行
        环绕通知返回值设置为Object 类型
        环绕通知中可以对原始方法调用过程中出现的异常进行处理
返回后通知
抛出异常后通知

5.4 通知中获取参数

获取切入点方法的参数,所有的通知类型都可以获取参数
        JoinPoint:适用于前置、后置、返回后、抛出异常后通知
        
        ProceedingJoinPoint:适用于环绕通知
获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究
        返回后通知
        环绕通知
获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究
        抛出异常后通知
        
        环绕通知

6.AOP事务管理

6.1Spring事务简介

6.1.1相关概念介绍

事务作用:在数据层保障一系列的数据库操作同成功同失败
Spring 事务作用:在数据层或 业务层 保障一系列的数据库操作同成功同失败
数据层有事务我们可以理解,为什么业务层也需要处理事务呢 ?
举个简单的例子,
转账业务会有两次数据层的调用,一次是加钱一次是减钱
把事务放在数据层,加钱和减钱就有两个事务
没办法保证加钱和减钱同时成功或者同时失败
这个时候就需要将事务放在业务层进行处
Spring 为了管理事务,提供了一个平台事务管理器 PlatformTransactionManager
commit 是用来提交事务, rollback 是用来回滚事务。
PlatformTransactionManager 只是一个接口, Spring 还为其提供了一个具体的实现 :

 

从名称上可以看出,我们只需要给它一个 DataSource 对象,它就可以帮你去在业务层管理事
务。
其内部采用的是 JDBC 的事务。所以说如果你持久层采用的是 JDBC 相关的技术,就可以采用
这个 事务管理 器来管理你的事务。而 Mybatis 内部采用的就是 JDBC 的事务,所以后期我们
Spring 整合 Mybatis 采用的这个 DataSourceTransactionManager 事务管理器。

6.1.3转账案例-环境搭建

步骤 1: 准备数据库表

步骤2:创建项目导入jar

项目的 pom.xml 添加相关依赖
  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.1.16</version>
    </dependency>

    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.6</version>
    </dependency>

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.47</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>

    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.3.0</version>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>

  </dependencies>
步骤 3: 根据表创建模型类
import java.io.Serializable;

public class Account implements Serializable {

    private Integer id;
    private String name;
    private Double money;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Double getMoney() {
        return money;
    }

    public void setMoney(Double money) {
        this.money = money;
    }

    @Override
    public String toString() {
        return "Account{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", money=" + money +
                '}';
    }
}
步骤 4: 创建 Dao 接口
public interface AccountDao {

    @Update("update tbl_account set money = money + #{money} where name = #{name}")
    void inMoney(@Param("name") String name, @Param("money") Double money);

    @Update("update tbl_account set money = money - #{money} where name = #{name}")
    void outMoney(@Param("name") String name, @Param("money") Double money);
}
步骤 5: 创建 Service 接口和实现类
public interface AccountService {
    /**
     * 转账操作
     * @param out 传出方
     * @param in 转入方
     * @param money 金额
     */
    public void transfer(String out,String in ,Double money) ;
}
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    public void transfer(String out,String in ,Double money) {
        accountDao.outMoney(out,money);
        accountDao.inMoney(in,money);
    }

}
步骤 6: 添加 jdbc.properties 文件
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=root
步骤 7: 创建 JdbcConfig 配置类
public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String userName;
    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource dataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(userName);
        ds.setPassword(password);
        return ds;
    }
}
步骤 8: 创建 MybatisConfig 配置类
public class MybatisConfig {

    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        ssfb.setTypeAliasesPackage("com.itheima.domain");
        ssfb.setDataSource(dataSource);
        return ssfb;
    }

    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage("com.itheima.dao");
        return msc;
    }
}

步骤 9: 创建 SpringConfig 配置类
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
public class SpringConfig {
}
步骤 10: 编写测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {

    @Autowired
    private AccountService accountService;

    @Test
    public void testTransfer() throws IOException {
        accountService.transfer("Tom","Jerry",100D);
    }

}

程序运行分析:由上到下进行分析,首先在测试类中,我们需要对业务层的转账操作进行事务管

理,transfer()方法是对数据层的outMoney()和inMoney()进行调用,数据层使用注解开发并且使用

@Parm注解对参数进行定位,在domain中,Account继承了Serializable接口从而实现了数据序列

化,在SpringConfig中,四个注解分别声明了该类是配置类,扫描指定的类到IOC容器中,声明数

据源文件,导入其他两个配置类,MybatisCofing是整合Mybatis所需的xml配置文件,使用配置类

进行替代,而JdbcCofig是为了加载jdbc.properties配置文件,该类的形式是为了解耦。

6.1.4事务管理

以上的程序并没有转账进行事务管理,当程序报错时会出现进出不对账。

Spring 事务管理具体的实现步骤为 :
步骤 1: 在需要被事务管理的方法上添加注解
public interface AccountService {
    /**
     * 转账操作
     * @param out 传出方
     * @param in 转入方
     * @param money 金额
     */
    //配置当前接口方法具有事务
    @Transactional
    public void transfer(String out,String in ,Double money) ;
}
注意 :
@Transactional 可以写在接口类上、接口方法上、实现类上和实现类方法上
写在接口类上,该接口的所有实现类的所有方法都会有事务
写在接口方法上,该接口的所有实现类的该方法都会有事务
写在实现类上,该类中的所有方法都会有事务
写在实现类方法上,该方法上有事务
建议写在实现类或实现类的方法上
步骤 2: JdbcConfig 类中配置事务管理器
public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String userName;
    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource dataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(userName);
        ds.setPassword(password);
        return ds;
    }

    //配置事务管理器,mybatis使用的是jdbc事务
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource){
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }


}
注意: 事务管理器要根据使用技术进行选择, Mybatis 框架使用的是 JDBC 事务,可以直接使用
DataSourceTransactionManager
步骤 3 :开启事务注解
SpringConfig 的配置类中开启
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}
步骤 4: 运行测试类
会发现在转换的业务出现错误后,事务就可以控制回顾,保证数据的正确性。

6.2 Spring事务角色

这节中我们重点要理解两个概念,分别是 事务管理员 事务协调员
1. 未开启 Spring 事务之前 :

 

AccountDao outMoney 因为是修改操作,会开启一个事务 T1
AccountDao inMoney 因为是修改操作,会开启一个事务 T2
AccountService transfer 没有事务,
        运行过程中如果没有抛出异常,则T1 T2 都正常提交,数据正确
        如果在两个方法中间抛出异常,T1 因为执行成功提交事务, T2 因为抛异常不会被执行
        就会导致数据出现错误

2. 开启Spring的事务管理后

 

transfer 上添加了 @Transactional 注解,在该方法上就会有一个事务 T
AccountDao outMoney 方法的事务 T1 加入到 transfer 的事务 T
AccountDao inMoney 方法的事务 T2 加入到 transfer 的事务 T
这样就保证他们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准
确性。

事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法

事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法

6.3 Spring事务属性

6.3.1 事务配置

 

上面这些属性都可以在 @Transactional 注解的参数上进行设置
readOnly true 只读事务, false 读写事务,增删改要设为 false, 查询设为 true
timeout: 设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚, -1 表示不设
置超 时时间。
rollbackFor: 当出现指定异常进行事务回滚
noRollbackFor: 当出现指定异常不进行事务回滚
思考:出现异常事务会自动回滚,这个是我们之前就已经知道的
noRollbackFor 是设定对于指定的异常不回滚,这个好理解
rollbackFor 是指定回滚异常,对于异常事务不应该都回滚么,为什么还要指定 ?
这块需要更正一个知识点,并不是所有的异常都会回滚事务,比如下面的代码就不会回滚(比
如IOException)

出现这个问题的原因是, Spring 的事务只会对 Error 异常 RuntimeException 异常
其子类进行事务回顾,其他的异常类型是不会回滚的,对应 IOException 不符合上述条
件所以不回滚
此时就可以使用 rollbackFor 属性来设置出现 IOException 异常不回滚

rollbackForClassName等同于rollbackFor,只不过属性为异常的类全名字符串

noRollbackForClassName 等同于 noRollbackFor ,只不过属性为异常的类全名字符串
isolation设置事务的隔离级别
DEFAULT : 默认隔离级别 , 会采用数据库的隔离级别
READ_UNCOMMITTED : 读未提交
READ_COMMITTED : 读已提交
REPEATABLE_READ : 重复读取
SERIALIZABLE: 串行化

6.3.2 转账业务追加日志案例

在前面的转案例的基础上添加新的需求,完成转账后记录日志。
需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕
需求微缩: A 账户减钱, B 账户加钱,数据库记录日志

需要注意一点就是,我们这个案例的预期效果为 :
无论转账操作是否成功,均进行转账操作的日志留痕
1.事务传播行为:

 2.事务传播行为的可选值

 要想实现上述的结果,我们只需让日志在一个单独的事务中,使用@Transactional注解中的propagation = Propagation.REQUIRES_NEW属性。

public interface LogService {
    //propagation设置事务属性:传播行为设置为当前操作需要新事务
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    void log(String out, String in, Double money);
}

 

相关文章:

  • 时序与空间结构
  • 一幅长文细学TypeScript(一)——上手
  • DM JDBC
  • hadoop2.2.0开机启动的后台服务脚本(请结合上一篇学习)
  • java基于springboot+vue的学生成绩管理系统 elementui
  • 测试与开发环境网址hosts配置
  • MogDB企业应用 之 Rust驱动
  • html css面试题
  • 密码学 | RC4算法Native层分析
  • 融合与创新:数据堂骨龄标注工具为医生赋能
  • MySQL:库操作 | 表操作
  • 【VINS-Mono】
  • 【Vue】Axios取消请求
  • 小白如何在ios中安装ios上架
  • 云视频协作平台有哪些 云视频在线审片解决方法
  • [译] 怎样写一个基础的编译器
  • 【391天】每日项目总结系列128(2018.03.03)
  • 【JavaScript】通过闭包创建具有私有属性的实例对象
  • C++入门教程(10):for 语句
  • flask接收请求并推入栈
  • java小心机(3)| 浅析finalize()
  • pdf文件如何在线转换为jpg图片
  • Synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比...
  • vuex 笔记整理
  • Vue实战(四)登录/注册页的实现
  • yii2中session跨域名的问题
  • 将 Measurements 和 Units 应用到物理学
  • 近期前端发展计划
  • 少走弯路,给Java 1~5 年程序员的建议
  • 十年未变!安全,谁之责?(下)
  • MiKTeX could not find the script engine ‘perl.exe‘ which is required to execute ‘latexmk‘.
  • Java数据解析之JSON
  • TPG领衔财团投资轻奢珠宝品牌APM Monaco
  • Unity3D - 异步加载游戏场景与异步加载游戏资源进度条 ...
  • ​secrets --- 生成管理密码的安全随机数​
  • # 睡眠3秒_床上这样睡觉的人,睡眠质量多半不好
  • #《AI中文版》V3 第 1 章 概述
  • #绘制圆心_R语言——绘制一个诚意满满的圆 祝你2021圆圆满满
  • (09)Hive——CTE 公共表达式
  • (13):Silverlight 2 数据与通信之WebRequest
  • (function(){})()的分步解析
  • (PWM呼吸灯)合泰开发板HT66F2390-----点灯大师
  • (求助)用傲游上csdn博客时标签栏和网址栏一直显示袁萌 的头像
  • (三) prometheus + grafana + alertmanager 配置Redis监控
  • (续)使用Django搭建一个完整的项目(Centos7+Nginx)
  • (学习日记)2024.03.25:UCOSIII第二十二节:系统启动流程详解
  • .mysql secret在哪_MYSQL基本操作(上)
  • .mysql secret在哪_MySQL如何使用索引
  • .NET 依赖注入和配置系统
  • /dev/VolGroup00/LogVol00:unexpected inconsistency;run fsck manually
  • /proc/stat文件详解(翻译)
  • ;号自动换行
  • @EventListener注解使用说明
  • [2013][note]通过石墨烯调谐用于开关、传感的动态可重构Fano超——
  • [20160902]rm -rf的惨案.txt