spring 注解学习

342 阅读12分钟

一、bean 注解的使用

在不使用spring注解之前,我们如果要往 IOC 容器中放入一个 java Bean ,需要以下几个步骤:

  • 创建一个spring的配置文件 bean.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">


    <bean id="person" class="com.liuqiuyi.demo.beans.Person">
        <property name="name" value="刘秋意"/>
        <property name="age" value="24"/>
    </bean>

</beans>
  • 创建 spring ioc 容器,从容器中获取我们配置的 person 对象

    public class SpringStartTest {
        public static void main(String[] args) {
            // 指定配置文件所在的目录
            ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml");
            Person person = (Person) applicationContext.getBean("person");
            System.out.println(person);
        }
    }
    

使用注解方式完成以上操作:

  • 首先我们需要新建一个配置类 MainConfig ,添加上对应的注解,例如:
/**
* 配置类信息
*
* @author liuqiuyi
* @date 2020-04-28
*/
@Configuration  // 需要标识这是一个配置类
public class MainConfig {
  /**
   * ‘@Bean’ 注解标识这是一个 javaBean
   * 方法名称默认为 bean在ioc容器中的id。bean类型为该方法的返回值类型
   *
   * @author liuqiuyi
   * @date 2020-04-28
   */
  @Bean
  public Person person() {
     return new Person("刘秋意", 24);
 }
}
  • 在创建 ioc 容器时,需要使用 AnnotationConfigApplicationContext

    public class SpringStartTest {
        public static void main(String[] args) {
            // 注意使用的是 AnnotationConfigApplicationContext
            ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
            // 直接通过类型获取Bean
            Person person = applicationContext.getBean(Person.class);
            System.out.println(person);
        }
    }
    

MainConfig 中的 person 方法的方法名称就是ioc中bean的id,可以使用 @Bean(value = "person") 修改

二、包扫描注解

之前使用xml方式的时候,我们需要在xml文件中进行如下的配置:

<!-- 配置包扫描,如果添加了 @Controller @Service @Repository @Component ,就能将对应的类加载到ioc容器中 -->
<context:component-scan base-package="com.liuqiuyi.demo"/>

现在通过注解的方式进行配置:

  • 在配置类 MainConfig 上添加 @ComponentScan 扫描注解,效果和上面的xml配置一样
/**
* 配置类信息
*
* @author liuqiuyi
* @date 2020-04-28
*/
@Configuration  // 需要标识这是一个配置类
@ComponentScan(value = "com.liuqiuyi.demo")
public class MainConfig {
    /**
     * ‘@Bean’ 注解标识这是一个 javaBean,方法名称默认为 bean在ioc容器中的id
     *
     * @author liuqiuyi
     * @date 2020-04-28
     */
    @Bean(value = "person")
    public Person person() {
        return new Person("刘秋意", 24);
    }
}
  • @ComponentScan 注解可以指定扫描时的排除规则,例如:
/**
* 配置类信息
*
* @author liuqiuyi
* @date 2020-04-28
*/
@Configuration  // 需要标识这是一个配置类
@ComponentScan(value = "com.liuqiuyi.demo", excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class)})
public class MainConfig {
    /**
     * ‘@Bean’ 注解标识这是一个 javaBean,方法名称默认为 bean在ioc容器中的id
     *
     * @author liuqiuyi
     * @date 2020-04-28
     */
    @Bean(value = "person")
    public Person person() {
        return new Person("刘秋意", 24);
    }
}

以上的配置表示,在进行包扫描时,根据注解排除掉 @Controller ,这样加了 @Controller 注解的就不会被加载到 IOC 容器中

  • 还可以通过 includeFilters 指定只加载哪些注解,但是需要将 useDefaultFilters 的值设置为false (为true时表示默认加载所有的配置,如果我们想自定义加载哪些注解,就需要设置为 false),具体的可以看下 @ComponentScan 的具体实现
  • @ComponentScans 注解里面可以指定多种扫描策略

三、Scope 作用域的配置

之前我们可以在 xml 配置中指定 javaBean 的 作用域,默认为单例的,我们可以修改为多例的,例如:

<!-- 修改bean的作用域为多例的 -->
<bean id="person" class="com.liuqiuyi.demo.beans.Person" scope="prototype">
    <property name="name" value="刘秋意"/>
    <property name="age" value="24"/>
</bean>

现在我们可以使用对应的注解完成,在 MainConfig 中 bean配置上加上 @Scope("prototype") ,默认是 singleton 单例的

/**
* 配置类信息
*
* @author liuqiuyi
* @date 2020-04-28
*/
@Configuration  // 需要标识这是一个配置类
@ComponentScan(value = "com.liuqiuyi.demo", excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class)})
public class MainConfig {
    /**
     * ‘@Bean’ 注解标识这是一个 javaBean,方法名称默认为 bean在ioc容器中的id
     *
     * @author liuqiuyi
     * @date 2020-04-28
     */
    @Bean(value = "person")
    @Scope("prototype")  // 设置bean的作用域为多例的
    public Person person() {
        return new Person("刘秋意", 24);
    }
}

四、Lazy 懒加载配置

如果我们希望某一个bean在IOC容器初始化的时候不要被创建,有以下两种方式:

  • 通过上面说得的 Scope 注解将bean配置为多例的
  • 如果不能将bean的作用域设置为多例的,可以添加 @Lazy 注解,注意这个只针对单例的场景
/**
* 配置类信息
*
* @author liuqiuyi
* @date 2020-04-28
*/
@Configuration  // 需要标识这是一个配置类
@ComponentScan(value = "com.liuqiuyi.demo", excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class)})
public class MainConfig {
    /**
     * ‘@Bean’ 注解标识这是一个 javaBean,方法名称默认为 bean在ioc容器中的id
     *
     * @author liuqiuyi
     * @date 2020-04-28
     */
    @Bean(value = "person")
    @Scope("prototype")  // 设置bean的作用域为多例的
    @Lazy  // 默认值为true
    public Person person() {
        return new Person("刘秋意", 24);
    }
}

五、Conditional 按照条件注册

在不同的运行环境下,我需要注入不同的bean对象,这个时候可以使用 @Conditional 注解,例如:如果是Windows环境下,我需要注入 Person 对象,如果是linux环境下,我就不注入这个Person对象

  • 创建一个 WindowsCondition 类,实现 Condition 接口,重写里面的方法:

    public class WindowsCondition implements Condition {
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            Environment environment = context.getEnvironment();
            // 获取运行环境
            String property = environment.getProperty("os.name");
            System.out.println("========: " + property);
            // 如果运行环境名称中包含 Windows ,则返回 true
            return property.contains("Windows");
        }
    }
    
  • 在配置文件对应的bean上加上注解

    @Configuration  // 需要标识这是一个配置类
    @ComponentScan(value = "com.liuqiuyi.demo", excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class)})
    public class MainConfig {
        /**
         * ‘@Bean’ 注解标识这是一个 javaBean,方法名称默认为 bean在ioc容器中的id
         *
         * @author liuqiuyi
         * @date 2020-04-28
         */
        @Bean(value = "person")
        @Scope("prototype")  // 设置bean的作用域为多例的
        @Lazy
        @Conditional(value = WindowsCondition.class)
        public Person person() {
            return new Person("刘秋意", 24);
        }
    }
    

    @Conditional 注解也可以放在 MainConfig 类上

六、Import 配置要加入IOC的bean

除了上面说的 @ComponentScan 注解和 @Bean 注解之外,spring还提供了 @Import 注解,示例:

@Configuration
@Import(Blue.class)  // 这里表示将 Blue 这个类加载到IOC容器中
public class MainConfig {
    /**
     * ‘@Bean’ 注解标识这是一个 javaBean,方法名称默认为 bean在ioc容器中的id
     *
     * @author liuqiuyi
     * @date 2020-04-28
     */
    @Bean(value = "person")
    @Scope("prototype")  // 设置bean的作用域为多例的
    @Lazy
    @Conditional(value = WindowsCondition.class)
    public Person person() {
        return new Person("刘秋意", 24);
    }
}

@Import 注解添加的Bean,bean 的id为类的全路径,例如:com.liuqiuyi.demo.beans.Blue

如果我们一次性要添加很多个,可以配合 ImportSelector 接口使用:

  • 新建一个 MyImportSelector 类,需要实现 ImportSelector ,重写对应的方法:

    public class MyImportSelector implements ImportSelector {
        @Override
        public String[] selectImports(AnnotationMetadata importingClassMetadata) {
            // 返回要导入的Bean,bean id 也是类的全路径
            return new String[]{"com.liuqiuyi.demo.beans.Red"};
        }
    }
    
  • 在 @Import 注解上加上这个 MyImportSelector 类

    @Configuration  // 需要标识这是一个配置类
    @Import({Blue.class, MyImportSelector.class})
    public class MainConfig {
        /**
         * ‘@Bean’ 注解标识这是一个 javaBean,方法名称默认为 bean在ioc容器中的id
         *
         * @author liuqiuyi
         * @date 2020-04-28
         */
        @Bean(value = "person")
        @Scope("prototype")  // 设置bean的作用域为多例的
        @Lazy
        @Conditional(value = WindowsCondition.class)
        public Person person() {
            return new Person("刘秋意", 24);
        }
    }
    

通过 ImportSelector 接口能够完成Bean的注入,但是我们无法指定名称,这时可以使用 ImportBeanDefinitionRegistrar 接口完成自定义的bean注入

  • 新建一个 MyImportBeanDefinitionRegistrar 类,实现 ImportBeanDefinitionRegistrar 接口

    public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            // 如果IOC容器中不包含 red 这个Bean,注入 Red 对象
            if (!registry.containsBeanDefinition("red")) {
                // 新建一个BeanDefinition对象
                BeanDefinition definition = new RootBeanDefinition(Red.class);
                registry.registerBeanDefinition("red", definition);
            }
        }
    }
    
  • 使用时,在 @Import 注解后加上 MyImportBeanDefinitionRegistrar 这个类

    @Configuration  // 需要标识这是一个配置类
    @Import({Blue.class, MyImportBeanDefinitionRegistrar.class})
    public class MainConfig {
        /**
         * ‘@Bean’ 注解标识这是一个 javaBean,方法名称默认为 bean在ioc容器中的id
         *
         * @author liuqiuyi
         * @date 2020-04-28
         */
        @Bean(value = "person")
        @Scope("prototype")  // 设置bean的作用域为多例的
        @Lazy
        @Conditional(value = WindowsCondition.class)
        public Person person() {
            return new Person("刘秋意", 24);
        }
    }
    

七、使用 FactoryBean 注册Bean

我们也可以使用 spring 提供的 FactorBean 来注册对应的bean

  • 写一个 MyFactoryBean 类,实现 FactoryBean接口,接口的泛型为返回的Bean类型

    public class MyFactoryBean implements FactoryBean<Red> {
        @Override
        public Red getObject() throws Exception {
            return new Red();
        }
        
        @Override
        public Class<?> getObjectType() {
            return Red.class;
        }
    
        /**
         * 返回false表示不是单例
         * 返回true表示是单例
         */
        @Override
        public boolean isSingleton() {
            return true;
        }
    }
    
  • 在对应的配置文件 MainConfig 中加上对应的 Bean 扫描

    @Configuration  // 需要标识这是一个配置类
    public class MainConfig {
        @Bean("myFactoryBean")
        public MyFactoryBean myFactoryBean() {
            return new MyFactoryBean();
        }
    }
    

注意:

  • 从 IOC 容器中获取 bean id 为 myFactoryBean 的对象时,返回的并不是工厂类本身,而是工厂类中返回的对象,例如:

    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
    Object myFactoryBean = applicationContext.getBean("myFactoryBean");
    System.out.println("类型为:" + myFactoryBean); // com.liuqiuyi.demo.beans.Red@2f8dad04
    System.out.println("原始类型为:" + applicationContext.getBean("&myFactoryBean")); //com.liuqiuyi.demo.beans.MyFactoryBean@29e495ff
    

    如果我们需要获取工厂类 myFactoryBean 本身,那么需要在 bean id 前面加上一个 “&” 符号

八、配置 bean 初始化 和 销毁方法

之前在使用 xml 配置 bean的时候,可以指定 bean 初始化时调用的方法和销毁时调用的方法:

<bean id="person" class="com.liuqiuyi.demo.beans.Person" scope="prototype" init-method="method_a" destroy-method="method_b">
    <property name="name" value="刘秋意"/>
    <property name="age" value="24"/>
</bean>

对应的注解写法为:

@Bean(value = "person",initMethod = "init",destroyMethod = "destroy")
public Person person() {
    return new Person("刘秋意", 24);
}

注意:

  • 构造方法在初始化方法之前执行
  • bean的作用域为单例的情况下,容器初始化时会调用bean的初始化方法,容器关闭时会调用bean的销毁方法
  • bean的作用域为多例的情况下,容器初始化时不会调用bean的初始化方法,在获取bean的时候才会调用初始化方法,并且容器关闭是不调用bean的销毁方法的

除了上面的实现方式,我们还可以通过让 bean 实现 InitializingBean, DisposableBean 接口,重写里面的 afterPropertiesSet() 和 destroy() 方法

public class Person implements InitializingBean, DisposableBean {
    public Person() {
    }
    /**
     * 创建bean时调用的初始化方法
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("初始化方法。。。。");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("销毁时方法。。。。");
    }
}

第三种实现方法,可以使用 @PostConstruct 和 @PreDestroy 注解完成对应的功能

public class Person {
    public Person() {
    }
    /**
     * 创建bean时调用的初始化方法
     */
    @PostConstruct
    public void init() throws Exception {
        System.out.println("初始化方法。。。。");
    }

    @PreDestroy
    public void destroy() throws Exception {
        System.out.println("销毁时方法。。。。");
    }
}

九、BeanPostProcessor 前置处理器接口

spring 提供了 BeanPostProcessor 接口,用于在 bean 初始化之前 和 初始化完成 之后回调的方法,

  • 新建一个类 MyBeanPostProcessor ,实现 BeanPostProcessor 接口,重写对应的方法

    public class MyBeanPostProcessor implements BeanPostProcessor {
    
        /**
         * 在bean的初始化方法之前调用
         */
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            System.out.println("前置处理器,在初始化前调用 。。。" + beanName);
                   // 这个bean可以直接返回,也可以包装后返回,但是不能返回null 
            return bean;
        }
    
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            System.out.println("前置处理器,在初始化后调用 。。。" + beanName);
            return bean;
        }
    }
    

这个 MyBeanPostProcessor 添加了之后,只要是 spring 管理的 bean ,在初始化之前后都会调用 前置处理器中的方法,包括 spring 框架自身的 bean 也会调用

十、Value 注解

如果我们想给 bean 的属性注入值,之前 xml 的配置中,我们可以使用 标签设置对应的值,例如:

<bean id="person" class="com.liuqiuyi.demo.beans.Person" scope="prototype">
    <property name="name" value="刘秋意"/>
    <property name="age" value="24"/>
</bean>

我们也可以使用 @Value 注解完成这个操作,例如:

  • 新建一个Book类,有三种方法赋值

    public class Book {
        /**
         * 可以手动直接赋值
         */
        @Value("JAVA从入门到放弃")
        private String name;
    
        /**
         * 可以使用 ${} 从配置文件中读取值,需要读取对应的配置文件
         */
        @Value("${book.author}")
        private String author;
    
        /**
         * 可以使用spring SpEL 表达式设置值
         */
        @Value("#{100-20}")
        private Integer price;
        
        // 省略 构造方法 和 get/set 方法
    }
    
  • 如果使用了第二种 ${} 方式赋值,我们需要读取对应的配置文件,此处在 resources 下新建一个配置类 book.preperties

注意 book.preperties 的文件编码,一定要是 UTF-8 的,否则会出现中文乱码

  • 在 spring 配置类上,我们还需要加上 @PropertySource 注解,需要告诉 spring 要从哪里读取文件:
@Configuration
@PropertySource(value = {"classpath:book.properties"},encoding = "UTF-8")
public class TestConfig {
    @Bean("book")
    public Book book() {
        return new Book();
    }
}

十一、@Autowired 自动注入注解

  • 默认优先按照类型去属性中找对应的bean,如果有多个类型的bean,在将属性名称作为bean id 去容器中查找

  • 可以使用 @Qualifier 注解根据指定的 bean id 去容器中查找,而不是使用属性名称

  • @Autowired 注解添加之后,默认是一定要找到对应的 bean 的,否则容器启动过程中会报错,如果我们想在有 bean 的情况下注入,没有的情况下不注入,可以设置对应的 required 属性为 false

    @Autowired(required = false)
    BookService bookService
    
  • 也可以使用 @Primary 注解标明哪个 bean 的优先级更高,这样在根据属性自动注入时,就会优先匹配加了 @Primary 注解的 bean

    @Service
    @Primary
    public class BookA implements Books
    
    @Service
    public class BookB implements Books
    
    @Autowired
    Books book  //此时会优先匹配BookA
    

@Resource 和 @Inject 注解:

  • 这两个注解都是 java 提供的,@Resource(JSP250)规范提供,@Inject(JSP330)规范提供
  • @Resource:可以和@Autowired一样实现自动装配的功能,但是默认是按照 bean 名称进行装配的;并且不能支持 @Primary 和 required = false 的功能
  • @Inject:需要导入javax.inject的包,能支持 @Primary 功能,但是不支持 required = false 的功能

另外,@Autowired 注解还可以加在 构造器、参数、方法和属性上,这些情况下都是从容器中获取值,例如:

  • 标注在方法位置:

    @Component
    public class Blue {
        private Red red;
    
        public Red getRed() {
            return red;
        }
    
        /**
         * autowired 注解可以不加,都是默认从ioc容器中取值
         */
        @Autowired
        public void setRed(Red red) {
            // 不加 autowired 注解 或者 加在这个地方都可以实现从ioc容器中取值
            //    public void setRed(@Autowired Red red) {
            this.red = red;
        }
    }
    
  • 标在构造器上:

    @Component
    public class Blue {
        private Red red;
    
        /**
         * autowired 注解可以不加,都是默认从ioc容器中取值
         */
        @Autowired
        public Blue(Red red) {
            // 不加 autowired 注解 或者 加在这个地方都可以实现从ioc容器中取值
    //    public Blue(@Autowired Red red) {
            this.red = red;
        }
    }
    

十二、xxxAware 接口

如果我们自定义的 bean 想要使用 spring 容器的底层组件(ApplicationContext,BeanFactory 等等),可以让自定义的 bean 实现实现对应的 xxxAware 接口,例如:

@Component
public class Blue implements ApplicationContextAware {
    /**
     * 使用一个成员变量接收
     */
    private ApplicationContext applicationContext;

    /**
     * 如果我们想使用 ApplicationContext ,可以实现对应的 ApplicationContextAware 接口
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
  • ApplicationContextAware 接口是 Aware 接口的子类,Aware 接口还有其它的实现类来完成不同的功能,使用方法和 ApplicationContextAware 类似,Aware 接口的实现类如下:

    image-20201222232132701

  • xxxAware 接口实现原理:使用到了后置处理器,ApplicationContextAware 之所以能够完成将 ApplicationContext 注入到自定义类中,是因为有对应的后置处理器 ApplicationContextAwareProcessor 来实现

image-20201222232205834

image-20201222232225189

十三、@Profile 注解

@profile 注解是 spring 提供的可以根据当前环境,动态切换和激活一系列组件的功能。接下来模拟一个不同环境下链接不同数据库的场景:

  • 编写一个配置类 DateSourceConfig 和一个配置文件类 dateSource.properties
@Configuration
@PropertySource(value = "classpath:dateSource.properties", encoding = "UTF-8")
public class DateSourceConfig {
    @Value("${username}")
    private String useName;

    @Value("${password}")
    private String password;

    @Value("${dev.hosts.url}")
    private String devUrl;

    @Value("${test.hosts.url}")
    private String testUrl;

    @Bean("devData")
    @Profile("dev") // 表示在dev环境下激活
    public DruidDataSource devDataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl(devUrl);
        druidDataSource.setUsername(useName);
        druidDataSource.setPassword(password);
        druidDataSource.setDriverClassName("com.mysql.jdbc.Driver");
        return druidDataSource;
    }

    @Bean("testData")
    @Profile("test") // 表示在test环境下激活
    public DruidDataSource testDataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl(testUrl);
        druidDataSource.setUsername(useName);
        druidDataSource.setPassword(password);
        druidDataSource.setDriverClassName("com.mysql.jdbc.Driver");
        return druidDataSource;
    }
}

dateSource.properties 内容为:

username=root
password=123456
dev.hosts.url=jdbc:mysql://localhost:3306/my_demo
test.hosts.url=jdbc:mysql://localhost:3306/my_test
  • 启动容器的时候,需要配置 JVM 启动参数,指定激活的是哪个环境

    -Dspring.profiles.active=dev
    
  • 除了设置 JVM 启动参数,我们也可以直接在代码中进行设置:

    public class SpringStartTest {
        public static void main(String[] args) {
            // 注意要用 AnnotationConfigApplicationContext
            AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
            // 获取系统运行环境
            ConfigurableEnvironment environment = applicationContext.getEnvironment();
            //设置启动的环境为dev
            environment.setActiveProfiles("dev");
            // 设置读取的配置文件
            applicationContext.register(DateSourceConfig.class);
            // 刷新容器
            applicationContext.refresh();
    
            String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
            for (String beanDefinitionName : beanDefinitionNames) {
                System.out.println(beanDefinitionName);
            }
        }
    }
    

@profile 也可以设置在类上,这里不做演示

十四、AOP 注解相关

spring AOP:指在程序运行期间动态的将某段代码切入到指定方法的指定位置 spring 的通知方法有:

  • 前置通知 @Before :在目标方法运行之前执行
  • 后置通知 @After:在目标方法结束之后运行,无论方法是正常结束还是异常结束
  • 返回通知 @AfterReturning:在目标方法正常返回之后运行
  • 异常通知 @AfterThrowing:在目标方法出现异常之后运行
  • 环绕通知 @Around:手动推进目标方法运行 joinPoint.procced()

下面有个小例子简单演示下:

  • 导入对应的依赖 spring-aspects
  • 首先编写一个切面类 AopAspects
@Aspect // 申明这是一个切面类
@Component
@Order(value = 2) // 设置我们切面的优先级为2,因为spring 事务就是使用aop做的,所以我们的优先级要小于事务的aop
public class AopAspects {
    /**
     * 切点表达式,此处我们直接使用一个自定义的注解 @AopTest,这里也可以使用切点表达式写
     */
    @Pointcut("@annotation(com.liuqiuyi.demo.aop.AopTest)")
    public void pointCut() {
    }

    @Before("pointCut()")
    public void beforeMethod(JoinPoint joinPoint) {
        System.out.println(joinPoint.getSignature().getName() + " 方法开始执行,入参为:" + Arrays.toString(joinPoint.getArgs()));
    }

    @After("pointCut()")
    public void afterMethod(JoinPoint joinPoint) {
        System.out.println(joinPoint.getSignature().getName() + " 方法执行完成");
    }

    /**
     * 注意这里 JoinPoint 一定要放在参数第一个
     * result 表示我们使用这个参数接收返回值
     */
    @AfterReturning(value = "pointCut()", returning = "result")
    public void afterReturningMethod(JoinPoint joinPoint, Object result) {
        System.out.println(joinPoint.getSignature().getName() + " 方法执行完成,返回的结果为:" + result);
    }

    @AfterThrowing(value = "pointCut()", throwing = "e")
    public void afterThrowingMethod(JoinPoint joinPoint, Exception e) {
        System.out.println(joinPoint.getSignature().getName() + " 方法执行发生异常,异常信息为:" + e.getMessage());
    }

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) {
        System.out.println(joinPoint.getSignature().getName() + " 方法开始执行,入参为:" + Arrays.toString(joinPoint.getArgs()));
        Object proceed = null;
        try {
            proceed = joinPoint.proceed();
        } catch (Throwable throwable) {
            System.out.println(joinPoint.getSignature().getName() + " 方法执行出现异常,异常信息为:" + throwable);
        }
        System.out.println(joinPoint.getSignature().getName() + " 方法开始执行,出参为:" + proceed);
        return proceed;
    }
}

其中,自定义的注解类为 AopTest

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AopTest {
}
  • 新建一个配置类 AopConfig,注意一定要加上 @EnableAspectJAutoProxy 注解
@Configuration
@EnableAspectJAutoProxy // 如果使用AOP,一定要加上这个配置,表示开启aop的自动代理
@ComponentScan(value = "com.liuqiuyi.demo.aop")
public class AopConfig {
}
  • 编写一个目标类和目标方法
@Component
public class CalculateTest {
    @AopTest
    public int div(int a, int b) {
        return a / b;
    }
}
  • 测试方法:
public class AopMainTest {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AopConfig.class);
        // 注意不要直接new一个目标类,需要从ioc容器中获取
        CalculateTest bean = applicationContext.getBean(CalculateTest.class);
        bean.div(10, 0);
    }
}

总结下来三步:

  • 将目标类和切面类都加入到 ioc 容器中,并且告诉 spring 哪个是切面类(通过@Aspect注解标明)
  • 在切面类的方法上,标明通知方式,申明切点表达式,告诉 spring 什么时候在哪运行
  • 开启基于注解的AOP模式(在配置类上加上 @EnableAspectJAutoProxy 注解 )

十五、spring 注解式事务实现

  • 导入相关的依赖:数据源、数据库驱动、spring-jdbc 模块
  • 配置数据源、jdbcTemplate,开启基于注解的事务管理功能 @EnableTransactionManagement
@Configuration
@EnableTransactionManagement // 开启基于注解的事务管理功能
@ComponentScan(value = "com.liuqiuyi.demo.aop")
public class AopConfig {
    @Bean
    public DataSource dataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl("jdbc:mysql://localhost:3306/my_demo");
        druidDataSource.setUsername("root");
        druidDataSource.setPassword("123456");
        druidDataSource.setDriverClassName("com.mysql.jdbc.Driver");
        return druidDataSource;
    }

    /**
     * 配置事务管理器
     */
    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }
}
  • 在需要事务控制的方法上添加 @Transactional 注解