SSM框架-Spring(二)

目录

1 手写spring框架

2 Spring IoC注解式开发

2.1 回顾注解

2.2 声明Bean的注解

2.3 Spring注解的使用

2.4 选择性实例化Bean

2.5 负责注入的注解

2.5.1 @Value

2.5.2 @Autowired与@Qualifier

2.5.3 @Resource

2.6 全注解式开发

3 JdbcTemplate

3.1 环境准备

3.2 新增

3.3 修改

3.4 删除

3.5 查询一个对象

3.6 查询多个对象

3.7 查询一个值

3.8 批量添加

3.9 使用回调函数

3.10 使用德鲁伊连接池

4 GoF之代理模式

4.1 代理模式的理解

4.2 静态代理

4.3 动态代理

4.3.1 JDK动态代理

4.3.2 CGLIB动态代理

5 面向切面编程AOP

5.1 AOP介绍

5.2 AOP的七大术语

5.3 切点表达式

5.4 使用Spring的AOP

5.4.1 准备工作

5.4.2 基于AspectJ的AOP注解式开发

5.4.3 基于XML配置方式的AOP(了解)

5.5 AOP实际开发案例:事务处理

5.6 AOP的实际案例:安全日志


1 手写spring框架

下次一定写

2 Spring IoC注解式开发

2.1 回顾注解

注解的存在是为了简化XML的配置。Spring6倡导全注解式开发

我们先回顾一下:注解怎么定义、使用

注解的定义:

package com.itzw.annotationn;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(value = {ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
    String value();
}
  • 以上是自定义了一个注解Component
  • 该注解上面修饰的注解包括:Target注解和Retention注解,这俩注解被称为元注解
  • Target注解用来设置Component注解出现的位置,以上表示只能出现在类和接口上
  • Retention注解用来设置Component注解的保持性策略,以上表示该注解可以被反射机制读取
  • String value()是Component注解的一个属性,该属性类型是String,属性名是value

注解的使用:

package com.itzw.annotationn;

@Component("hello")
public class Hello {
}

用法简单,如果属性名为value,那么value可以省略不写

2.2 声明Bean的注解

负责声明Bean的注解,常见的包括四个:

  • @Component
  • @Controller
  • @Service
  • @Repository

我们通过源码发现,这后三个注解都是第一个注解的别名,也就是说,这四个注解其实功能是一样的,用哪个都可以。但是为了强调程序的可读性,建议:

  • 控制器类上使用:Controller
  • service类上使用:Service
  • dao类上使用:Repository

它们都只有一个value属性,value属性用来指定bean的id,也就是bean的名字

2.3 Spring注解的使用

如何使用以上注解呢?

  • 第一步:加入aop依赖
  • 第二步:在配置文件中添加context命名空间
  • 第三步:在配置文件中指定扫描的包
  • 第四步:在Bean类上使用注解

第一步:加入aop依赖

当我们加入spring-context依赖之后,会关联aop依赖

SSM框架-Spring(二)_第1张图片

第二步:在配置文件中添加context命名空间




第三步:在配置文件中指定扫描的包

第四步:在Bean类上使用注解

package com.itzw.spring6.dao;

import org.springframework.stereotype.Component;

@Component("user")
public class User {
}

测试程序:

    @Test
    public void testAnnotation(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        User user = applicationContext.getBean("user", User.class);
        System.out.println(user);
    }

结果:

值得注意的是:该注解是可以不赋值的,默认值为该类名的首字母小写

package com.itzw.spring6.dao;

import org.springframework.stereotype.Component;

@Component
public class User {
}

去掉赋的值测试依然没问题

我们将Component注解换成其它三个注解试一下。经测试都是可以使用的

如果有多个包怎么办:

  • 第一种:在配置文件中指定多个包,用逗号隔开
  • 第二种:指定多个包的共同父包

我们先来测试第一种方式:

经测试没问题

我们再来第二种方式:

这两个包都有一个共同的父包,所以我们直接把包名写为父包:

但这样会降低运行效率

2.4 选择性实例化Bean

因为我们有四个注解都可以用来帮助我们实例化Bean,但假如我只想实例化某一个注解下的Bean类呢?这种应用很少。

我们先实例化全部:

package com.itzw.spring6.dao;

import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;

@Component
public class A {
    public A(){
        System.out.println("A的无参构造执行了");
    }
}

@Controller
class B {
    public B(){
        System.out.println("B的无参构造执行了");
    }
}

@Service
class C {
    public C(){
        System.out.println("C的无参构造执行了");
    }
}

@Repository
class D {
    public D(){
        System.out.println("D的无参构造执行了");
    }
}



    

    @Test
    public void testSelect(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-select.xml");
    }

假如我只想实例化注解Controller下的Bean类:



    
    
        
    

  • use-default-filters="true" 表示:使用spring默认的规则,只要有Component、Controller、Service、Repository中的任意一个注解标注,则进行实例化。
  • use-default-filters="false" 表示:不再spring默认实例化规则,即使有Component、Controller、Service、Repository这些注解标注,也不再实例化。
  • 表示只有Controller进行实例化。

也可以将use-default-filters设置为true(不写就是true),并且采用exclude-filter方式排出哪些注解标注的Bean不参与实例化:

因为use-default-filters默认就是true,所以我们可以不写,这里我们排除Repository

    
        
    

2.5 负责注入的注解

前面只是讲到实例化Bean,但是我们是需要给参数传值的,也就是注入

给Bean属性赋值需要用到这些注解:

  • @Value
  • @Autowired
  • @Qualifier
  • @Resource

2.5.1 @Value

当属性是简单类型时可以使用@Value进行注入

package com.itzw.spring6.dao;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;

@Repository
public class Student {
    @Value("张麻子")
    private String name;
    @Value("34")
    private int age;

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}



    

测试结果: 

没什么毛病

通过以上方法注入我们发现不需要提供setter方法就可以注入了,以前白学了。所以以后我们 直接在属性上进行注入就可以

但其实我们依然可以提供setter方法然后在setter方法上使用@Value进行注入,但这是何必呢,硬给自己找事干?所以这里我们直接不演示这种方法了。同样的,它也可以在构造方法参数前进行注入,但是还是那句话,我们直接在属性上注入就好,简单又方便啊兄弟们。

2.5.2 @Autowired与@Qualifier

@Autowired注解可以用来注入非简单类型。被翻译为:自动连线的,或者自动装配。

单独使用@Autowired,默认根据类型装配。就是我们之前学的根据类型自动装配

查看它的源码:

package org.springframework.beans.factory.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
	boolean required() default true;
}

我们可以得出:该注解可以标注在:构造方法、方法、形参、属性、注解上;该注解有一个required属性,默认值是true,表示在注入的时候要求被注入的Bean必须是存在的,如果不存在则报错。如果required的值设为false,表示注入的Bean存在或者不存在都没关系,存在就注入,不存在也不报错。

我们先在属性上使用@Autowired注解:

package com.itzw.spring6.dao;

public interface OrderDao {
    void insert();
}
package com.itzw.spring6.dao.impl;

import com.itzw.spring6.dao.OrderDao;
import org.springframework.stereotype.Repository;

@Repository
public class OrderDaoImpl implements OrderDao {
    @Override
    public void insert() {
        System.out.println("正在插入信息。。");
    }
}
package com.itzw.spring6.service;

import com.itzw.spring6.dao.OrderDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private OrderDao orderDao;

    public void add(){
        orderDao.insert();
    }
}



    

测试没有问题.

我们没有提供setter方法也没有提供构造方法,只是在要注入的属性上写了一个注解就可以。当然我们也可以在setter方法上使用注解,在构造方法上使用,在构造方法的参数上使用。甚至当我们创建了构造方法后都不用写这个注解,但只限于一个构造方法。

其实这么多方式进行注入,还是使用最简单的在属性上写注解就好了。

之前说了这个注解默认是根据类型(byType)注入的,但如果OrderDao接口还有一个类呢?

package com.itzw.spring6.dao.impl;

import com.itzw.spring6.dao.OrderDao;
import org.springframework.stereotype.Repository;

@Repository
public class OrderDaoImplByOracle implements OrderDao {
    @Override
    public void insert() {
        System.out.println("oracle数据库正在连接。。");
    }
}

我们再进行测试,这时会出错

那怎么解决呢?使用byName,根据name进行装配

@Autowired注解和@Qualifier注解联合起来才可以根据名称进行装配,在@Qualifier注解中指定Bean名称。

SSM框架-Spring(二)_第2张图片

总结: 

  • @Autowired注解可以出现在:属性上、构造方法上、构造方法参数上、setter方法上
  • @Autowired注解默认根据类型注入。如果 需要根据名称注入的话,需要配合@Qualifier使用

2.5.3 @Resource

  • @Resource注解也可以完成非简单类型的注入,那它和@Autowired注解有什么区别呢?
  • @Resource注解是JDK扩展包中的,也就是说属于JDK的一部分。所以该注解是标准注解,更具有通用性。
  • @Autowired注解是spring框架自己的
  • @Resource注解默认根据名称装配byName,未指定name时,使用属性名作为name。如果name找不到的话会自动启动通过类型byType装配。
  • @Autowired注解默认根据类型装配byName,如果想根据名称装配,需配合@Qualifier主机一起
  • @Resource注解用在属性、setter方法上
  • @Autowired注解用在属性上、setter方法上、构造方法上、构造方法参数上

@Resource注解属于JDK扩展包,所以不在JDK当中,需要额外引入已下依赖:(如果是JDK8的话不需要引入依赖,高于JDK11或者低于JDK8需要引入以下依赖)


  jakarta.annotation
  jakarta.annotation-api
  2.1.1

一定要注意:如果你用Spring6,要知道Spring6不再支持JavaEE,它支持的是JakartaEE9。(Oracle把JavaEE贡献给Apache了,Apache把JavaEE的名字改成JakartaEE了,大家之前所接触的所有的 javax.* 包名统一修改为 jakarta.*包名了。)

我这里使用的就是spring6,所以用上面的依赖就可以,如果是spring6以下的用别的依赖

我们直接修改之前的代码,给类OrderDaoImplByOracle的名字命名为“xyz”

SSM框架-Spring(二)_第3张图片

在service文件中使用@Resource

SSM框架-Spring(二)_第4张图片

测试没有问题

我们再修改dao类的名字,使用默认名字,@Resource注解也去掉名字,使用默认:这样会报错

我们再修改dao类的名字,使用service文件中属性的名字命名,也就是orderDao:

SSM框架-Spring(二)_第5张图片

这样可以正常运行,通过测试得知,当@Resource注解使用时没有指定name的时候,还是根据name进行查找,这个name是属性名。如果还找不到会根据类型(byType)查找,如果还找不到就报错

2.6 全注解式开发

所以前面我们用配置文件用了半天结果我们不需要用了,白学公主就是我。

我们需要写一个配置类代替配置文件:

package com.itzw.spring6.config;

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

@Configuration
@ComponentScan({"com.itzw.spring6.dao","com.itzw.spring6.service"})
public class Spring6Configuration {
}

其中@ComponentScan里面填写需要使用到的包名

编写测试程序,不再new ClassPathXmlApplicationContext()对象了。

    @Test
    public void testNoXML(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);
        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
        orderService.add();
    }

3 JdbcTemplate

JdbcTemplate是spring提供的一个JDBC模板类,是对JDBC的封装,简化JDBC代码,当然也可以不用,可以让spring继承其它的ORM框架,如:mybatis、hibernate等。

3.1 环境准备

数据库准备:

SSM框架-Spring(二)_第6张图片

idea准备,添加依赖:

    
        
            repository.spring.milestone
            Spring Milestone Repository
            https://repo.spring.io/milestone
        
    

    
        
            org.springframework
            spring-context
            6.0.0-M2
        
        
            junit
            junit
            4.13.2
            test
        
        
        
            mysql
            mysql-connector-java
            8.0.30
        
        
        
            org.springframework
            spring-jdbc
            6.0.0-M2
        
    

表t_user对应的User实体类:

package com.itzw.spring6.bean;

public class User {
    private Integer id;
    private String realName;
    private Integer age;

    public User() {
    }

    public User(Integer id, String realName, Integer age) {
        this.id = id;
        this.realName = realName;
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

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

    public String getRealName() {
        return realName;
    }

    public void setRealName(String realName) {
        this.realName = realName;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", realName='" + realName + '\'' +
                ", age=" + age +
                '}';
    }
}

编写spring配置文件: 

JdbcTemplate是spring提供好的类,完整名为:org.springframework.jdbc.core.JdbcTemplate,怎么使用这个类呢?直接new对象就可以,直接将这个类配置到spring配置文件中,纳入bean管理。



    

我们查看这个类的源码发现它有个属性为DataSource,这个属性是数据源。连接数据库需要Connection对象,而生成Connection对象是数据源负责的,所以我们需要给JdbcTemplate设置数据源属性。并且所有数据源都是要实现javax.sql.DataSource接口的,这个数据源可以自己写一个,当然也可以用写好的,比如:阿里巴巴的德鲁伊连接池,c3p0,dbcp等,我们这里先自己手写一个数据源。

MyDataSource类:重点写getConnection方法

package com.itzw.spring6.jdbc;

import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

public class MyDataSource implements DataSource {

    private String driver;
    private String url;
    private String username;
    private String password;

    public String getDriver() {
        return driver;
    }

    public void setDriver(String driver) {
        this.driver = driver;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }


    // 重点写怎么获取Connection对象就行。其他方法不用管。
    @Override
    public Connection getConnection() throws SQLException {
        try {
            Class.forName(driver);
            Connection conn = DriverManager.getConnection(url, username, password);
            return conn;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return null;
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        return null;
    }

    @Override
    public void setLogWriter(PrintWriter out) throws SQLException {

    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {

    }

    @Override
    public int getLoginTimeout() throws SQLException {
        return 0;
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return null;
    }

    @Override
    public  T unwrap(Class iface) throws SQLException {
        return null;
    }

    @Override
    public boolean isWrapperFor(Class iface) throws SQLException {
        return false;
    }
}

在spring配置文件注入数据,并把数据源注入给JdbcTemplate:

    
        
        
        
        
    
    
    
        
    

这样环境就准备完了,下面开始增删改查

3.2 新增

    @Test
    public void testInsert(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-jdbc.xml");
        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        String sql = "insert into t_user(real_name,age) value(?,?)";
        int count = jdbcTemplate.update(sql, "张三", 12);
        System.out.println(count);
    }

注意:update执行的两个参数:第一个参数是需要执行的SQL语句;第二个参数是可边长参数,可以 是0个也可以是多个,一般是SQL语句有几个问号对应几个参数。

只要是对数据库表有修改的SQL语句如增删改都是使用update方法。

3.3 修改

    @Test
    public void testUpdate(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-jdbc.xml");
        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        String sql = "update t_user set real_name = ?,age = ? where id = ?";
        int count = jdbcTemplate.update(sql, "lisi", 22,4);
        System.out.println(count);
    }

3.4 删除

    @Test
    public void testDelete(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-jdbc.xml");
        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        String sql = "delete from t_user where id = ?";
        int count = jdbcTemplate.update(sql, 4);
        System.out.println(count);
    }

3.5 查询一个对象

    @Test
    public void testSelect(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-jdbc.xml");
        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        String sql = "select * from t_user where id = ?";
        User user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), 1);
        System.out.println(user);
    }

queryForObject方法三个参数:

  • 第一个:SQL语句
  • 第二个:Bean属性值和数据库记录行的映射对象,在构造方法中指定映射的 对象类型
  • 第三个:可边长参数,给SQL语句的占位符问号传值。

3.6 查询多个对象

    @Test
    public void testSelectAll(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-jdbc.xml");
        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        String sql = "select * from t_user";
        List users = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
        System.out.println(users);
    }

查询多个把queryForObject方法换成query方法。

3.7 查询一个值

查询表的记录条数

    @Test
    public void testSelectNumber(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-jdbc.xml");
        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        String sql = "select count(1) from t_user";
        Integer count = jdbcTemplate.queryForObject(sql, int.class);
        System.out.println(count);
    }

此时queryForObject方法参数需要标明返回的数据类型

3.8 批量添加

    @Test
    public void testBatchInsert(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-jdbc.xml");
        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        String sql = "insert into t_user(real_name,age) value(?,?)";
        Object[] obj1 = {"张三1",12};
        Object[] obj2 = {"张三2",13};
        Object[] obj3 = {"张三3",14};
        List list = new ArrayList<>();
        list.add(obj1);
        list.add(obj2);
        list.add(obj3);
        int[] count = jdbcTemplate.batchUpdate(sql, list);
        System.out.println(Arrays.toString(count));
    }

还有批量修改批量删除,类似

3.9 使用回调函数

非常的愚蠢,不用

3.10 使用德鲁伊连接池

之前的数据源是我们自己写的,我们也可以使用别人写的:

第一步:引入德鲁伊连接池依赖


  com.alibaba
  druid
  1.1.8

第二步:将德鲁伊中的数据源配置到spring配置文件中。和配置我们自己写的一样。

    
        
        
        
        
    

    
        
    

我们测试,查询表的记录条数:

4 GoF之代理模式

4.1 代理模式的理解

比如演员需要替身,这里的替身可以代替演员完成表演,替身就是代理类,演员就是目标类,它们都是为了完成一个事情,所以继承的是同一个接口。在这里演员也就是代理类可以做到保护演员的作用;可以完成演员完成不了的动作实现了增强作用;当演员有事的时候,可以实现代替作用。

总的来说代理模式的作用是为其它对象提供一种代理以控制对这个对象的访问。在某些情况下,一个客户不想或者不能直接引用一个对象,可以通过“代理对象”实现间接引用,并且它可以添加额外的服务。

代理模式中的角色:

  • 代理类(代理主题)
  • 目标类(真实主题)
  • 代理类和目标类的公共接口(抽象主题)

代理模式在代码的实现上有两种形式:

  • 静态代理
  • 动态代理

4.2 静态代理

有以下接口和实现类:

package com.itzw.proxy.service;

public interface OrderService {
    //生成订单
    void generate();
    //查看订单
    void detail();
    //修改订单
    void modify();
}
package com.itzw.proxy.service;

public class OrderServiceImpl implements OrderService{
    @Override
    public void generate() {
        try {
            Thread.sleep(1234);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单正在生成。。");
    }

    @Override
    public void detail() {
        try {
            Thread.sleep(234);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("正在查看订单。。");
    }

    @Override
    public void modify() {
        try {
            Thread.sleep(123);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("正在修改订单。。");
    }
}

我们模拟用户使用客户端测试:

package com.itzw.proxy.client;

import com.itzw.proxy.service.OrderService;
import com.itzw.proxy.service.OrderServiceImpl;

public class Test {
    public static void main(String[] args) {
        OrderService orderService = new OrderServiceImpl();
        orderService.generate();
        orderService.detail();
        orderService.modify();
    }

}

假如现在用户有新需求,程序运行太慢了。这时候我们想找出哪个功能用时最长,该怎么找出这个功能

第一种方式:直接修改源代码

这样最简单,但是违背了编程的OCP开闭原则,我们不想修改源代码

第二种方案:编写一个子类继承OrderServiceImpl,在子类中重写每个方法,代码如下:

package com.itzw.proxy.service;

public class OrderServiceImplSub extends OrderServiceImpl{
    @Override
    public void generate() {
        long start = System.currentTimeMillis();
        super.generate();
        long end = System.currentTimeMillis();
        System.out.println("用时:" + (end - start) + "秒");
    }

    @Override
    public void detail() {
        long start = System.currentTimeMillis();
        super.detail();
        long end = System.currentTimeMillis();
        System.out.println("用时:" + (end - start) + "秒");
    }

    @Override
    public void modify() {
        long start = System.currentTimeMillis();
        super.modify();
        long end = System.currentTimeMillis();
        System.out.println("用时:" + (end - start) + "秒");
    }
}

这种方式确实可以解决这个问题,但是也有缺点:

假设系统有100个这样的业务,需要提供100个子类,会产生类爆炸;由于是继承的方式,导致代码之间的耦合度比较高。

第三种方式:使用代理模式

为OrderService接口提供一个代理类:

package com.itzw.proxy.service;

public class OrderServiceProxy implements OrderService{

    private OrderService orderService;

    public OrderServiceProxy(OrderService orderService) {
        this.orderService = orderService;
    }

    @Override
    public void generate() {
        long start = System.currentTimeMillis();
        orderService.generate();
        long end = System.currentTimeMillis();
        System.out.println("用时:" + (end - start) + "秒");
    }

    @Override
    public void detail() {
        long start = System.currentTimeMillis();
        orderService.detail();
        long end = System.currentTimeMillis();
        System.out.println("用时:" + (end - start) + "秒");
    }

    @Override
    public void modify() {
        long start = System.currentTimeMillis();
        orderService.modify();
        long end = System.currentTimeMillis();
        System.out.println("用时:" + (end - start) + "秒");
    }
}
        OrderService target = new OrderServiceImpl();
        OrderService orderService = new OrderServiceProxy(target);
        orderService.generate();
        orderService.detail();
        orderService.modify();

这样使用的是关联关系而不是继承关系,关联度低了很多。但是我们发现这样依然会产生类爆炸,一个接口对应一个代理类显然不合理。动态代理可以解决这个问题,在动态代理中可以在内存中动态的为我们生成代理类的字节码,代理类不需要我们写了

4.3 动态代理

在程序运行阶段,在内存中动态生成代理类,被称为动态代理,目的是为了减少代理类的数量。

在内存中动态生成类的技术常见的包括:

  • JDK动态代理技术:只能代替接口
  • CGLIB动态代理技术:CGLIB(Code Generation Library)是一个开源项目,它既可以代理接口,又可以代理类,底层是通过继承的方式实现的,性能比 JDK动态代理好
  • Javassit动态代理技术:这个我们在学mybatis的时候接触到过。

4.3.1 JDK动态代理

我们首先还是写一个接口和实现类,只不过这次不需要再写代理类了,这个可以动态生成,我们直接写客户端程序即可:

package com.itzw.proxy.client;

import com.itzw.proxy.service.OrderService;
import com.itzw.proxy.service.OrderServiceImpl;

import java.lang.reflect.Proxy;

public class Test {
    public static void main(String[] args) {
        //第一步:创建目标对象
        OrderService target = new OrderServiceImpl();
        //第二步:创建代理对象
        OrderService orderServiceProxy = Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),调用处理器对象);
        //第三步:调用代理对象的代理方法
        orderServiceProxy.generate();
        orderServiceProxy.detail();
        orderServiceProxy.modify();

    }
}

以上三步就能完成动态代理,尤其注意第二步:

这行代码做了两件事:

  • 第一件事:在内存中生成了代理类的字节码
  • 第二件事:创建代理对象

Proxy类全名是:java.lang.reflect.Proxy。这是JDK提供的一个类

其中newProxyInstance()方法中有三个参数:

  • 第一个参数:类加载器,在内存中生产了字节码,要想执行这个字节码,也是需要先把这个字节码加载到内存中的。所以要指定哪个类加载器加载。
  • 第二个参数:接口类型,代理类和目标类实现相同的接口,要通过这个参数告诉JDK动态代理生成的类要实现哪些接口。
  • 第三个参数:调用处理器,这是一个JDK动态代理规定的接口,全名为:java.lang.reflect.InvocationHandler。这是一个回调接口,也就是说调用这个接口中方法的程序已经写好了,就差这个接口的实现类了。

所以接下来要写java.lang.reflect.InvocationHandler接口的实现类,并且实现 接口中的方法,如下

package com.itzw.proxy.service;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class TimerInvocationHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return null;
    }
}

我们发现这个接口中有个方法invoke,这个invoke方法上有三个参数:

  • 第一个参数:Object proxy,代理对象
  • 第二个参数:Method method,目标方法
  • 第三个参数:Object[] args,目标方法调用时要传的参数

我们将来是要调用“目标方法”的,但要调用目标方法需要“目标对象”的存在,我们可以给这个接口提供一个构造方法,可以通过这个构造方法传过来“目标对象”,如下:

    //目标对象
    Object target;

    public TimerInvocationHandler(Object target) {
        this.target = target;
    }

有了目标对象我们就可以在invoke()方法中调用目标方法了。代码如下:

package com.itzw.proxy.service;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class TimerInvocationHandler implements InvocationHandler {
    //目标对象
    Object target;

    public TimerInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        //增强部分代码
        long start = System.currentTimeMillis();
        //调用目标对象的目标方法
        //还记得调用方法需要四要素:对象、方法名、参数、返回值
        Object retVal = method.invoke(target, args);
        //增强部分代码
        long end = System.currentTimeMillis();
        System.out.println("用时" + (end - start) + "毫秒");
        return retVal;
    }
}

目前为止调用处理器完成了,接下来完善client程序:

package com.itzw.proxy.client;

import com.itzw.proxy.service.OrderService;
import com.itzw.proxy.service.OrderServiceImpl;
import com.itzw.proxy.service.TimerInvocationHandler;

import java.lang.reflect.Proxy;

public class Test {
    public static void main(String[] args) {
        //第一步:创建目标对象
        OrderService target = new OrderServiceImpl();
        //第二步:创建代理对象
        OrderService orderServiceProxy = (OrderService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),new TimerInvocationHandler(target));
        //第三步:调用代理对象的代理方法
        orderServiceProxy.generate();
        orderServiceProxy.detail();
        orderServiceProxy.modify();
    }
}

目前为止,动态代理就完成了,但是有个小问题是,我们没有看见invoke方法被调用啊,注意当我们调用代理对象的代理方法时,invoke方法就会被调用,就是上面的第三步。

4.3.2 CGLIB动态代理

了解一下

5 面向切面编程AOP

AOP(Aspect Oriented Programming):面向切面编程(AOP是一种编程技术)

AOP底层使用的就是动态代理来实现的。

Spring的AOP使用的动态代理是:JDK动态代理+CGLIB动态代理技术。spring在这两种 动态代理中灵活切换,如果是代理接口,会默认使用JDK动态代理,如果要代理某个类,这个类没有实现接口,就会切换使用CGLIB。当然也可以配置让Spring只使用CGLIB。

5.1 AOP介绍

一般一个系统都会有一些系统服务,例如:日志、事务管理、安全等,这些系统服务被称为:交叉业务,这些交叉业务几乎是通用的

我们在上面使用的JDK动态代理就是面向AOP编程的

SSM框架-Spring(二)_第7张图片

5.2 AOP的七大术语

连接点 Joinpoint

  • 在程序的整个执行流程中,可以织入切面的位置。方法的执行前后,异常抛出之后等位置。

切点Pointcut

  • 在程序执行流程中,真正织入切面的方法。(一个切点对应多个连接点),切点就是方法

通知Advice

  • 通知又叫增强,就是具体你要织入的代码。
  • 通知包括:
    • 前置通知
    • 最终通知
    • 异常通知
    • 环绕通知
    • 后置通知

切面 Aspect

  • 切点 + 通知就是切面。

织入 Weaving

  • 把通知应用到目标对象上的过程。

代理对象Proxy

  • 一个目标对象被织入通知后产生的新对象

目标对象Target

  • 被织入通知的对象

5.3 切点表达式

切点表达式的格式:

execution([访问控制权限修饰符] 返回值类型 [全限定类名]方法名(形式参数列表) [异常])

访问控制权限修饰符:

  • 可选项。
  • 没写,就是4个权限都包括。
  • 写public就表示只包括公开的方法。

返回值类型:

  • 必填项。
  • * 表示返回值类型任意。

全限定类名:

  • 可选项。
  • 两个点“..”代表当前包以及子包下的所有类。
  • 省略时表示所有的类。

方法名:

  • 必填项。
  • *表示所有方法。
  • set*表示所有的set方法。

形式参数列表:

  • 必填项
  • () 表示没有参数的方法
  • (..) 参数类型和个数随意的方法
  • (*) 只有一个参数的方法
  • (*, String) 第一个参数类型随意,第二个参数是String的。

异常:

  • 可选项。
  • 省略时表示任意异常类型

5.4 使用Spring的AOP

Spring对AOP的实现包括一下3种方式:

  • 第一种方式:Spring框架结合AspectJ框架实现的AOP,基于注解方式
  • 第二种方式:Spring框架结合AspectJ框架实现的AOP,基于XML方式
  • 第三种方式:Spring框架自己实现的AOP,基于XML配置方式

实际开发中,都是Spring+AspectJ来实现AOP,所以我们重点学习第一种和第二种方式。

5.4.1 准备工作

使用Spring+AspectJ的AOP需要引入的依赖如下:

    
        
        
            org.springframework
            spring-context
            6.0.0-M2
        
        
        
            org.springframework
            spring-aspects
            6.0.0-M2
        
    

注意我们还需要aop依赖,因为context依赖包含了aop依赖,所以不需要单独引用了。

Spring配置文件中添加context命名空间和aop命名空间:




5.4.2 基于AspectJ的AOP注解式开发

实现步骤:

第一步:定义目标类和目标方法

package com.itzw.spring6.service;

public class OrderService {
    public void generate(){
        //目标方法
        System.out.println("订单正在生成。。");
    }
}

第二步:定义切面类,注意切面类上面要加注解@Aspect表示它是一个切面(通知+切点)

第三步:目标类和切面类都纳入spring bean管理

  • 在目标类OrderService上添加@Service注解。
  • 在切面类LogAspect类上添加@Component注解。

第四步:在spring配置文件中加入组件扫描

第五步:在切面类中添加通知(增强代码)

@Aspect
@Component
public class LogAspect {
    public void advice(){
        System.out.println("我是增强代码,我是一个通知");
    }
}

第六步:在通知上添加切点表达式(指定对哪个类的哪个方法进行增强也就是通知)

@Aspect
@Component
public class LogAspect {

    //切点表达式
    @Before("execution(* com.itzw.spring6.service.OrderService.*(..))")
    //这就是需要增强的代码,也就是通知
    public void advice(){
        System.out.println("我是增强代码,我是一个通知");
    }
}

注意:这里的Before注解表示前置通知,也就是在指定方法前进行通知(增强)

第七步:在spring配置文件中启用自动代理

  • 开启自动代理之后,凡是带有@Aspect注解的bean都会生成代理对象
  • 其中true表示采用CGLIB动态代理
  • false表示采用jdk动态代理,没有接口会选择CGLIB生成代理类,默认就是false

测试程序:

    @Test
    public void testBeforeAdvice(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aop.xml");
        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
        orderService.generate();
    }

通知类型包括:

  • 前置通知:@Before 目标方法执行之前通知
  • 后置通知:@AfterReturning 目标方法之后的通知
  • 环绕通知:@Around 目标方法之前和之后都添加通知
  • 异常通知:@AfterThrowing 发生异常之后的通知
  • 最终通知:@After 放在finally语句块中的通知

我们依次测试这些通知:

package com.itzw.spring6.service;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LogAspect {

/*    //切点表达式
    @Before("execution(* com.itzw.spring6.service.OrderService.*(..))")
    //这就是需要增强的代码,也就是通知
    public void beforeAdvice(){
        System.out.println("我是增强代码,我是一个通知");
    }*/

    @Before("execution(* com.itzw.spring6.service.OrderService.*(..))")
    public void beforeAdvice(){
        System.out.println("前置通知");
    }

    @AfterReturning("execution(* com.itzw.spring6.service.OrderService.*(..))")
    public void AfterReturningAdvice(){
        System.out.println("后置通知");
    }

    @Around("execution(* com.itzw.spring6.service.OrderService.*(..))")
    public void AroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕通知开始");
        //执行目标方法
        joinPoint.proceed();
        System.out.println("环绕通知结束");
    }

    @AfterThrowing("execution(* com.itzw.spring6.service.OrderService.*(..))")
    public void AfterThrowingAdvice(){
        System.out.println("异常通知");
    }

    @After("execution(* com.itzw.spring6.service.OrderService.*(..))")
    public void AfterAdvice(){
        System.out.println("最终通知");
    }

}

查看结果:

SSM框架-Spring(二)_第8张图片

这就是这些通知的顺序,但是没有异常通知,因为没有异常,我们模拟一手异常:

package com.itzw.spring6.service;

import org.springframework.stereotype.Service;

@Service
public class OrderService {
    public void generate(){
        //目标方法
        System.out.println("订单正在生成。。");
        if (true){
            throw new RuntimeException("模拟异常发生");
        }
    }
}

结果:

通过测试得知,当发生异常之后,最终通知也会执行,因为最终通知@After会出现在finally语句块中。出现异常之后,后置通知环绕通知的结束部分不会执行。

切面的顺序

我们知道,一个业务不可能只有一个切面,也就是说还存在例如安全控制、时间计算等切面,那它们的执行顺序怎样控制呢?可以使用Order注解,值越小优先级越高

我们再创建一个切面:

SSM框架-Spring(二)_第9张图片

SSM框架-Spring(二)_第10张图片

结果:

SSM框架-Spring(二)_第11张图片

优化使用切点表达式:

我们发现之前的代码中的切点表达式都是重复的,没有得到复用。并且如果要修改就要修改多处:

我们可以将切点表达式单独的定义出来,在需要的位置引入即可。如下:

package com.itzw.spring6.service;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Order(2)
public class LogAspect {

/*    //切点表达式
    @Before("execution(* com.itzw.spring6.service.OrderService.*(..))")
    //这就是需要增强的代码,也就是通知
    public void beforeAdvice(){
        System.out.println("我是增强代码,我是一个通知");
    }*/

    @Pointcut("execution(* com.itzw.spring6.service.OrderService.*(..))")
    public void pointcut(){}

    @Before("pointcut()")
    public void beforeAdvice(){
        System.out.println("Log'前置通知");
    }

    @AfterReturning("pointcut()")
    public void AfterReturningAdvice(){
        System.out.println("后置通知");
    }

    @Around("pointcut()")
    public void AroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕通知开始");
        //执行目标方法
        joinPoint.proceed();
        System.out.println("环绕通知结束");
    }

    @AfterThrowing("pointcut()")
    public void AfterThrowingAdvice(){
        System.out.println("异常通知");
    }

    @After("pointcut()")
    public void AfterAdvice(){
        System.out.println("最终通知");
    }

}

全注解式开发AOP

就是编写一个类,使用注解来代理之前的spring配置文件,因为我们已经使用注解了,而且大部分都用注解实现了,就一点点配置用到spring配置文件。这个我们在之前接触到过。如下:

package com.itzw.spring6.service;

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

@Configuration
@ComponentScan("com.itzw.spring6.service")
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class Spring6Configuration {
}

测试程序也需要修改一下:

    @Test
    public void testNoXml(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);
        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
        orderService.generate();
    }

5.4.3 基于XML配置方式的AOP(了解)

我们使用比较多的还是注解式开发,所以这个了解即可:

5.5 AOP实际开发案例:事务处理

一个项目肯定有很多业务,而这些业务极可能有多条DML语句,我们就需要使用事务处理避免出现错误,如果我们在每一个业务中都添加这些业务,显然很愚蠢,既然我们学了AOP,就使用AOP的方式进行事务处理。

有两个业务如下:

package com.itzw.spring6.service;

import org.springframework.stereotype.Service;

@Service
public class AccountService {
    public void tranfer(){
        System.out.println("正在转账。。。");
    }
    public void withdraw(){
        System.out.println("正在取款。。。");
    }
}
package com.itzw.spring6.service;

import org.springframework.stereotype.Service;

@Service
public class OrderService {
    public void generate(){
        System.out.println("正在生成订单。。");
    }
    public void cancel(){
        System.out.println("正在取消订单。。");
    }
}

以上两个业务都纳入spring bean的管理,下面我们使用AOP进行事务处理:

package com.itzw.spring6.service;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class TransactionAspect {
    
    @Around("execution(* com.itzw.spring6.service..*(..))")
    public void arountAdvice(ProceedingJoinPoint joinPoint){
        try {
            System.out.println("开启事务");
            //执行目标程序
            joinPoint.proceed();
            System.out.println("提交事务");
        } catch (Throwable e) {
            System.out.println("回滚事务");
        }
    }
}

这个事务只写了一次却能应用于多个业务

这里我们不配置spring配置文件了:

package com.itzw.spring6.service;

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

@Configuration
@ComponentScan("com.itzw.spring6.service")
@EnableAspectJAutoProxy
public class Spring6Configuration {
}

测试:

package com.itzw.spring6.test;

import com.itzw.spring6.service.AccountService;
import com.itzw.spring6.service.OrderService;
import com.itzw.spring6.service.Spring6Configuration;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class SpringAopTest {
    @Test
    public void test(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);
        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
        AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
        orderService.generate();
        orderService.cancel();
        accountService.tranfer();
        accountService.withdraw();
    }
}

结果:

SSM框架-Spring(二)_第12张图片

我们可以看到所有的业务方法都得到了事务控制。

SSM框架-Spring(二)_第13张图片

我们手动添加一个异常,再次测试:

SSM框架-Spring(二)_第14张图片

5.6 AOP的实际案例:安全日志

需求是这样的:项目开发结束了,已经上线了。运行正常。客户提出了新的需求:凡事在系统中进行修改操作的,删除操作的,新增操作的,都要把这个人记录下来。因为这几个操作是属于危险行为。例如有业务类和业务方法

先写两个业务:

package com.itzw.spring6.service2;

import org.springframework.stereotype.Service;

@Service
public class UserService {
    public void getUser(){
        System.out.println("获取用户信息");
    }
    public void saveUser(){
        System.out.println("保存用户信息");
    }
    public void deleteUser(){
        System.out.println("删除用户信息");
    }
    public void modifyUser(){
        System.out.println("修改用户信息");
    }
}
package com.itzw.spring6.service2;

import org.springframework.stereotype.Service;

@Service
public class ProductService {
    public void getProduct(){
        System.out.println("获取商品信息");
    }
    public void saveProduct(){
        System.out.println("保存商品信息");
    }
    public void deleteProduct(){
        System.out.println("删除商品信息");
    }
    public void modifyProduct(){
        System.out.println("修改商品信息");
    }
}

日志切面:

package com.itzw.spring6.service2;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

@Component
@Aspect
public class LogAspect {

    @Pointcut("execution(* com.itzw.spring6.service2..save*(..))")
    public void savePointcut(){}
    @Pointcut("execution(* com.itzw.spring6.service2..delete*(..))")
    public void deletePointcut(){}
    @Pointcut("execution(* com.itzw.spring6.service2..modify*(..))")
    public void modifyPointcut(){}

    @Before("savePointcut() || deletePointcut() || modifyPointcut()")
    public void beaforeAdvice(JoinPoint joinPoint){
        //系统时间
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
        String nowTime = sdf.format(new Date());
        System.out.println(nowTime+"张三:"+joinPoint.getSignature().getDeclaringTypeName()+"."+joinPoint.getSignature().getName());
    }
}
package com.itzw.spring6.service2;

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

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.itzw.spring6.service2")
public class Spring6Configuration {
}

测试:

    @Test
    public void test2(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);
        UserService userService = applicationContext.getBean("userService", UserService.class);
        ProductService productService = applicationContext.getBean("productService", ProductService.class);
        userService.getUser();
        userService.saveUser();
        userService.deleteUser();
        userService.modifyUser();
        
        productService.getProduct();
        productService.saveProduct();
        productService.deleteProduct();
        productService.modifyProduct();
    }

结果:

SSM框架-Spring(二)_第15张图片

你可能感兴趣的:(Java,spring,spring,java,后端)