Spring,Spring MVC, Spring Boot

文章目录


Spring

Spring 是包含了众多⼯具⽅法的 IoC 容器,Spring 的核心是IoC 和 AOP

Ioc(控制反转)

Ioc(控制反转)是一种思想不是一个技术实现,在类上⾯添加 @RestController 和 @Controller 注解, 就是把这个对象交给Spring管理, Spring 框架启动时就会加载该类. 把对象交给Spring管理, 就是IoC思想。

例如,现有类 A 依赖于类 B,类 B 依赖于类 C,对于传统的方式来说,类 A 中手动 new 一个 B 的对象,类 B 中手动 new 一个 C 的对象,此时当C中要添加一个 int 参数,那么类 B 也需要添加参数,同时连带 A 也需要做更改,这样的方式耦合度高。

对于Ioc方式来说,不通过 new 关键字来创建对象,而是通过 IoC 容器(Spring 框架) 来帮助我们实例化对象。我们需要哪个对象,直接从 IoC 容器里面去取即可。

改进之后获取依赖对象的过程进行了反转,不再是使用放对象创建并控制依赖对象了,而是把依赖对象注⼊将当前对象中,依赖对象的控制权不再由当前类控制了。


要把某个对象交给IOC容器管理,需要在类上添加⼀个注解@Component,而Spring框架为了更好的服务web应⽤程序,提供了更丰富的注解

  • 类注解:@Controller、@Service、@Repository、@Component、@Configuration.
  • 方法注解:@Bean

@Controller

下面的代码使用@Controller将Bean存储到Spring中

此时的Bean就是由Spring IoC容器实例化、组装和管理的对象

java 复制代码
import org.springframework.stereotype.Controller;

@Controller // 将对象存储到 Spring 中
public class UserController {
	public void sayHi(){
		System.out.println("UserController");
	}
}

@Service

java 复制代码
import org.springframework.stereotype.Service;

@Service
public class UserService {
	public void sayHi() {
		System.out.println("UserService");
	}

@Repository

java 复制代码
import org.springframework.stereotype.Repository;

@Repository
public class UserRepository {
	public void sayHi() {
		System.out.println("UserRepository");
	}
}

@Component

java 复制代码
import org.springframework.stereotype.Component;

@Component
public class UserComponent {
	public void sayHi() {
		System.out.println("UserComponent");
	}
}

@Configuration

java 复制代码
import org.springframework.context.annotation.Configuration;

@Configuration
public class UserConfiguration {
	public void sayHi() {
		System.out.println("UserConfiguration");
	}
}

类注解的适用性

注解 适用性情况
@Controller Controller控制层, 接收请求, 对请求进⾏处理, 并进⾏响应
@Service Service业务逻辑层, 处理具体的业务逻辑.
@Repository Dao数据访问层,也称为持久层. 负责数据访问操作
@Configuration 配置层. 处理项⽬中的⼀些配置信息
@Component 通用组件注解,任何被 Spring 管理的类都可以使用,是@Service、@Repository、@Controlle、r@Configuration的父注解

获取Bean的方式

下面的代码通过Spring上下⽂对象来获取Bean,获取方式包括:按照类型获取、按照名称获取、按照类型+名称获取。

@SpringBootApplication注解标记的这个SpringIocDemoApplication是项目的启动类

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {

	public static void main(String[] args) {
		//获取Spring上下⽂对象
		ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
		//根据bean类型, 从Spring上下⽂中获取对象
		UserController userController1 = context.getBean(UserController.class);
		//根据bean名称, 从Spring上下⽂中获取对象
		UserController userController2 = (UserController) context.getBean("userController");
		//根据bean类型+名称, 从Spring上下⽂中获取对象
		UserController userController3 = context.getBean("userController",UserController.class);
	
		//使⽤对象
		System.out.println(userController1);
		System.out.println(userController2);
		System.out.println(userController3);
	}
}

如果没有显式的提供bean名称(BeanId),Spring将为该bean⽣成唯⼀的名称。命名约定使⽤Java标准约定作为实例字段名,即bean名称以小写字⺟开头,然后使⽤驼峰式大小写;特殊的当第⼀个和第⼆个字符都是大写时, 将保留原始的大小写。
Bean的名称是唯一的,可以在注解后加括号双引号改名

通过getBean函数来获取Bean,其源代码如下,即通过BeanFactory来获取Bean

java 复制代码
public interface BeanFactory {
    String FACTORY_BEAN_PREFIX = "&";

    Object getBean(String var1) throws BeansException;

    <T> T getBean(String var1, Class<T> var2) throws BeansException;

    Object getBean(String var1, Object... var2) throws BeansException;

    <T> T getBean(Class<T> var1) throws BeansException;

    <T> T getBean(Class<T> var1, Object... var2) throws BeansException;
	
	......

面试问题:BeanFactory 和 ApplicationContext 的区别

1.继承关系和功能⽅⾯来说:Spring 容器有两个顶级的接⼝:BeanFactory 和 ApplicationContext。其中 BeanFactory 提供了基础的访问容器的能⼒,而ApplicationContext 属于 BeanFactory 的⼦类,它除了继承了 BeanFactory 的所有功能之外,它还拥有独特的特性,还添加了对国际化⽀持、资源访问⽀持、以及事件传播等⽅⾯的⽀持

2.从性能⽅⾯来说:ApplicationContext 是⼀次性加载并初始化所有的 Bean 对象,而BeanFactory 是需要那个才去加载那个,因此更加轻量. (空间换时间)

@Bean

@Bean用在配置类的方法上(通常是 @Configuration 类)。它的作用是告诉 Spring 容器,这个方法返回的对象,请把它纳入管理,成为一个 Bean。

java 复制代码
@Component
public class BeanConfig {
	@Bean
	public User user1(){
		User user = new User();
		user.setName("zhangsan");
		user.setAge(18);
		return user;
	}

	@Bean
	public User user2(){
		User user = new User();
		user.setName("lisi");
		user.setAge(19);
		return user;
	}
}

@Bean注解标识的对象,默认名称为方法名
同样的,在@Bean("name")中可以进行重命名

java 复制代码
@SpringBootApplication
public class SpringIocDemoApplication {
	public static void main(String[] args) {
		//获取Spring上下⽂对象
		ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
		//根据bean名称, 从Spring上下⽂中获取对象
		User user1 = (User) context.getBean("user1");
		User user2 = (User) context.getBean("user2");
		System.out.println(user1);
		System.out.println(user2);
	}
}

输出结果为:User(name=zhangsan,age=18) User(name=lisi,age=19)

扫描路径

Spring默认扫描的范围是SpringBoot启动类所在包及其⼦包,注解必须放在扫描路径下才能生效。

也可以通过 @ComponentScan 来配置扫描路径。

java 复制代码
@ComponentScan({"com.example.demo"})
@SpringBootApplication
public class SpringIocDemoApplication {

	public static void main(String[] args) {
		//获取Spring上下⽂对象
		ApplicationContext context = SpringApplication.run(SpringIocDemoApplication.class, args);
		//从Spring上下⽂中获取对象
		User u1 = (User) context.getBean("u1");
		//使⽤对象
		System.out.println(u1);
	}
}

DI(依赖注入)

DI(Dependency Injection,依赖注⼊)指容器在运⾏期间, 动态的为应⽤程序提供运⾏时所依赖的资源。Ioc是一种思想,DI 是IoC的⼀种实现。

  • 使用注解@Component来将类存入的容器中
  • 使用注解@Autowired来获取容器内的实例

属性注入

使用注解@Autowired来进行属性注入

java 复制代码
@Component
public class BookDao {
	public List<BookInfo> mockData() {
		......
		return books;
	}
}
java 复制代码
@Component
public class BookService {
	@Autowired
	private BookDao bookDao;

	public List<BookInfo> getBookList() {
		List<BookInfo> books = bookDao.mockData();
		......
		return books;
	}
}
java 复制代码
@RequestMapping("/book")
@RestController
public class BookController {
	@Autowired
	private BookService bookService;
	
	@RequestMapping("/getList")
	public List<BookInfo> getList(){
		List<BookInfo> books = bookService.getBookList();
		......
	return books;
}
}

构造方法注入

构造方法注入是在类的构造方法中实现注入

当只有一个构造函数时,Spring即使用该构造函数;如果有多个构造函数的时候,Spring会使用默认无参构造函数 ,如果没有默认的无参构造函数会报错。

可以通过添上 @Autowired 来明确指定到底使用哪个构造方法

java 复制代码
@Controller
public class UserController {
	//注⼊⽅法2: 构造⽅法
	private UserService userService;
	private UserConfig userConfig;

	public UserController() {
	}

	@Autowired
	public UserController(UserService userService) {
		this.userService = userService;
	}
	
	public UserController(UserService userService,UserConfig userConfig) {
		this.userService = userService;
		this.userConfig = userConfig;
	}

	public void sayHi(){
		System.out.println("hi,UserController2...");
		userService.sayHi();
		userConfig.sayHi();
	}
}

Setter 注入

Setter 注入和属性的 Setter 方法实现类似,只不过在设置 set 方法的时候需要加上@Autowired 注解

java 复制代码
@Controller
public class UserController3 {
	//注⼊⽅法3: Setter⽅法注⼊
	private UserService userService;
	
	@Autowired
	public void setUserService(UserService userService) {
		this.userService = userService;
	}
	
	public void sayHi(){
		System.out.println("hi,UserController3...");
		userService.sayHi();
	}
}

三种注入方式的比较

注入方式 优点 缺点
属性注入 简洁⽅便 只能⽤于 IoC 容器; 不能注⼊⼀个Final修饰的属性;
属性注入 可以注⼊final修饰的属性; 注⼊的对象不会被修改; 依赖对象在使⽤前⼀定会被完全初始化; 通⽤性好, 构造⽅法是JDK⽀持的, 更换框架后也适⽤; 注⼊多个对象时, 代码繁琐
Setter注入 ⽅便在类实例之后, 重新对该对象进⾏配置或者注⼊ 不能注⼊⼀个Final修饰的属性; setter⽅法可能会被多次调用, 注入对象有被修改的⻛险;

final修饰的属性,要么在声明时进行初始化,要么在构造函数中进行赋值

@Autowired存在的问题

当同⼀类型存在多个bean时, 使⽤@Autowired会存在问题,如下所示,通过UserController.class 获取 Bean 时,通过@Autowired注入user时,会先根据类型来获取,如果匹配不到就通过名称(此时名称为user)来获取,此时若存在多个对象,会出现报错,

java 复制代码
@Component
public class BeanConfig {
	@Bean("u1")
	public User user1(){
		User user = new User();
		user.setName("zhangsan");
		user.setAge(18);
		return user;
	}
	@Bean
	public User user2() {
		User user = new User();
		user.setName("lisi");
		user.setAge(19);
		return user;
	}
}
java 复制代码
@Controller
public class UserController {

	//注⼊user
	@Autowired
	private User user;
	
	public void sayHi(){
		System.out.println("UserController");
		System.out.println(user);
	}
}

针对这种问题,Spring提供了以下几种解决⽅案,

  • @Primary:当存在多个相同类型的Bean注⼊时,加上@Primary注解,来确定默认的实现
  • @Qualifier注解:指定当前要注⼊的bean对象。 在@Qualifier的value属性中,指定注⼊的bean的名称。 @Qualifier注解不能单独使用,必须配合@Autowired使用
  • @Resource注解:按照bean的名称进行注入。通过name属性指定要注⼊的bean的名称。
java 复制代码
@Component
public class BeanConfig {
	@Primary //指定该bean为默认bean的实现
	@Bean("u1")
	public User user1(){
		User user = new User();
		user.setName("zhangsan");
		user.setAge(18);
		return user;
	}
	@Bean
	public User user2() {
		User user = new User();
		user.setName("lisi");
		user.setAge(19);
		return user;
	}
}
java 复制代码
@Controller
public class UserController {
	@Qualifier("user2") //指定bean名称
	@Autowired
	private User user;
	
	public void sayHi(){
		System.out.println("UserController");
		System.out.println(user);
	}
}
java 复制代码
@Controller
public class UserController {

	@Resource(name = "user2")
	private User user;
	
	public void sayHi(){
		System.out.println("hi,UserController...");
		System.out.println(user);
	}
}

面试问题:@Resource 和 @Autowired 的区别

  1. @Autowired 是spring框架提供的注解,@Resource 是JDK提供的注解
  2. @Autowired 默认是按照类型进行注入,如果匹配不到就通过名称来获取,若名称有@Qualifier指定,则按照指定名称,若没有则按照默认的名称进行查找;@Resource是按照名称注入,比 @Autowired⽀持更多的参数设置,例如 name 设置,根据名称获取 Bean。

AOP(面向切面编程)

AOP(面向切面编程) 是 OOP(面向对象编程)的一种延续,二者互补,并不对立。AOP 允许把那些横跨多个业务模块的通用功能(比如日志记录、权限检查、事务管理)从业务代码中抽离出来,单独管理和复用,从而让业务代码更干净、更专注。

例如,现在有⼀些业务的执行效率比较低,耗时较长,需要对接口进行优化。需要统计当前项目中每⼀个业务方法的执行耗时。可以在方法前后记录时间并计算差值。但⼀个项目中会包含很多业务模块,每个业务模块又有很多接口,⼀个接口又包含很多方法, 如果要在每个业务方法中都记录会增加工作量。AOP就可以做到在不改动这些原始方法的基础上, 针对特定的方法进行功能的增强。

AOP的实现

下面是对Spring AOP的一个实现,先引入依赖

xml 复制代码
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

实现切面逻辑,@Aspect 标识这是⼀个切面类;@Around表示环绕通知,在目标方法的前后都会被执行为,后面的表达式表示对哪些方法进行增强。

java 复制代码
@Slf4j
@Aspect
@Component
public class TimeAspect {
    /**
     * 记录方法耗时
     */
    @Around("execution(* com.example.demo.controller.*.*(..))")
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        //记录方法执行开始时间
        long begin = System.currentTimeMillis();
        //执行原始方法
        Object result = pjp.proceed();
        //记录方法执行耗时
        log.info(pjp.getSignature() + "执行耗时: {}ms", System.currentTimeMillis() - begin);
        return result;
    }
}

AOP的核心概念

整个代码称之为一个切面,代码中相关定义如下,

  • @Around("execution(* com.example.demo.controller.*.*(...))")表示切点,双引号中的内容是切点表达式
  • ProceedingJoinPoint pjp表示连接点
  • long begin = System.currentTimeMillis();是目标方法执行前;
  • Object result = pjp.proceed();表示目标方法执行
  • log.info(pjp.getSignature() + "执行耗时: {}ms", System.currentTimeMillis() - begin);表示目标方法执行后
  1. 切点(Pointcut),也称之为"切入点"Pointcut 的作用就是提供一组规则 (使用 AspectJ pointcut expression language 来描述),告诉程序对哪些方法来进行功能增强。

  2. 连接点就是满足切点表达式规则的方法,也就是可以被AOP控制的方法。

  3. 通知:通知就是具体要做的工作,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)。比如上述程序中记录业务方法的耗时时间,就是通知。Spring中AOP的通知类型有以下几种:

    • @Around: 环绕通知, 此注解标注的通知方法在目标方法前, 后都被执行
    • @Before: 前置通知, 此注解标注的通知方法在目标方法前被执行
    • @After: 后置通知, 此注解标注的通知方法在目标方法后被执行, 无论是否有异常都会执行
    • @AfterReturning: 返回后通知, 此注解标注的通知方法在目标方法后被执行, 有异常不会执行
    • @AfterThrowing: 异常后通知,此注解标注的通知方法发生异常后执行
  4. 切面(Aspect) = 切点(Pointcut) + 通知(Advice)。通过切面就能够描述当前AOP程序需要针对于哪些方法,在什么时候执行什么样的操作。

现有Java程序如下,

java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo {
    //前置通知
    @Before("execution(* com.example.demo.controller.*.*(..))")
    public void doBefore() {
        log.info("执行AspectDemo.before 方法...");
    }

    //后置通知
    @After("execution(* com.example.demo.controller.*.*(..))")
    public void doAfter() {
        log.info("执行 AspectDemo.After 方法...");
    }

    //返回后通知
    @AfterReturning("execution(* com.example.demo.controller.*.*(..))")
    public void doAfterReturning() {
        log.info("执行 AspectDemo.AfterReturning 方法...");
    }

    //抛出异常后通知
    @AfterThrowing("execution(* com.example.demo.controller.*.*(..))")
    public void doAfterThrowing() {
        log.info("执行 doAfterThrowing 方法...");
    }

    //添加环绕通知
    @Around("execution(* com.example.demo.controller.*.*(..))")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("执行AspectDemo.doAround 目标方法前...");
        Object result = joinPoint.proceed();
        log.info("执行AspectDemo.doAround 目标方法后...");
        return result;
    }
}

方法正确执行后的日志打印如下,

方法异常情况执行的 打印情况如下,此时Object result = joinPoint.proceed();产生异常,doAround 目标方法后没有进行打印。

@PointCut

上面代码存在⼀个问题,就是存在大量重复的切点表达式Spring提供了 @PointCut 注解,把公共的切点表达式提取出来,需要用时引用该切入点表达式即可。

java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo {
    //定义切点(公共的切点表达式)
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    private void pt(){}
    
    //前置通知
    @Before("pt()")
    public void doBefore() {
        //...代码省略
    }

    //后置通知
    @After("pt()")
    public void doAfter() {
        //...代码省略
    }

    //返回后通知
    @AfterReturning("pt()")
    public void doAfterReturning() {
        //...代码省略
    }

    //抛出异常后通知
    @AfterThrowing("pt()")
    public void doAfterThrowing() {
        //...代码省略
    }

    //添加环绕通知
    @Around("pt()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        //...代码省略
    }
}

当切点定义使用 private 修饰时,仅能在当前切面类中使用,当其他切面类也要使用当前切点定义时,就需要把 private 改为public。引用方式为:全限定类名.方法名()

java 复制代码
public void pt(){}
java 复制代码
@Slf4j
@Component
@Aspect
public class AspectDemo2 {

    @Before("com.bite.aop.aspect.AspectDemo.pt()")
    public void doBefore(){
        log.info("执行AspectDemo2.before方法....");
    }

    @After("com.bite.aop.aspect.AspectDemo.pt()")
    public void doAfter(){
        log.info("执行AspectDemo2.after方法....");
    }
}

此时的运行结果如下,

切面优先级 @Order

当我们在⼀个项目中,定义了多个切面类时,,优先级默认按照名称排序,@Before 通知字母排名靠前的先执行; @After 通知:字母排名靠前的后执行。

可以通过@Order注解来控制这些切面通知的执行顺序,

java 复制代码
@Aspect
@Component
@Order(4)
public class AspectDemo2 {
    //...代码省略
}
java 复制代码
@Aspect
@Component
@Order(1)
public class AspectDemo3 {
    //...代码省略
}
java 复制代码
@Aspect
@Component
@Order(3)
public class AspectDemo4 {
    //...代码省略
}

打印结果如下,先执行AspectDemo3-4-2,再执行AspectDemo2-4-3;@Order 注解标识的切面类,执行顺序如下:@Before 通知:数字越小先执行;@After 通知:数字越大先执行

切点表达式

execution 切点表达式

execution() 是最常用的切点表达式,用来匹配方法,语法如下,其中访问修饰符和异常可以省略。

切点表达式支持通配符表达:

  • * 表示匹配任意字符,只匹配一个元素(返回类型, 包, 类名, 方法或者方法参数)
    • 包名使用 * 表示任意包(一层包使用一个*)
    • 类名使用 * 表示任意类
    • 返回值使用 * 表示任意返回值类型
    • 方法名使用 * 表示任意方法
    • 参数使用 * 表示一个任意类型的参数(类型固定,内容任意)
  • ... 表示匹配多个连续的任意符号, 可以通配任意层级的包, 或任意类型, 任意个数的参数
    a. 使用 ... 配置包名,标识此包以及此包下的所有子包
    b. 可以使用 ... 配置参数,任意个任意类型的参数
@annotation

execution表达式更适用于有规则的,如果要匹配多个无规则方法时,使用 execution 就不是很方便了。此时可以借助自定义注解的方式以及另一种切点表达式 @annotation 来描述这一类的切点,实现步骤如下:

  • 编写自定义注解
  • 使用 @annotation 表达式来描述切点
  • 在连接点的方法上添加自定义注解
java 复制代码
//@Target标明注解可以声明在类上还是方法上
//@Retention标明注解的生命周期

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {

}

使用 @annotation 切点表达式定义切点, 只对 @MyAspect ⽣效切类代码如下:

java 复制代码
@Slf4j
@Component
@Aspect
public class MyAspectDemo {
    //前置通知
    @Before("@annotation(com.example.demo.aspect.MyAspect)")
    public void before(){
        log.info("MyAspect -> before ...");
    }
    
    //后置通知
   @After("@annotation(com.example.demo.aspect.MyAspect)")
    public void after(){
        log.info("MyAspect -> after ...");
    }
}

在TestController中的t1()和UserController中的u1()这两个方法上添加⾃定义注解 @MyAspect , 其他方法不添加

java 复制代码
@MyAspect
@RequestMapping("/t1")
public String t1() {
    return "t1";
}
java 复制代码
@MyAspect
@RequestMapping("/u1")
public String u1(){
    return "u1";
}

面试问题:Spring AOP的实现方式

  1. 基于注解 @Aspect
  2. 基于⾃定义注解 (参考自定义注解 @annotation)
  3. 基于Spring API (通过xml配置的方式)
  4. 基于代理来实现

Spring AOP原理

代理模式

代理模式即为其他对象提供一种代理以控制对这个对象的访问。此时不再直接对目标方法进行调用,而是通过代理类间接调用。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

静态代理

静态代理指在程序运行前,代理类的 .class文件就已经存在了。(在出租房子之前,中介已经做好了相关的工作,就等租户来租房子了),如下例所示,

HouseSubject 接口定义了" 出租房子 "和" 售卖房子 "的行为

java 复制代码
// 1. 定义接口
public interface HouseSubject {
    void rentHouse();   // 出租房子
    void saleHouse();   // 出售房子
}

RealHouseSubject 表示真实的房东

java 复制代码
// 2. 真实房东实现接口
public class RealHouseSubject implements HouseSubject {
    @Override
    public void rentHouse() {
        System.out.println("我是房东, 我出租房子");
    }

    @Override
    public void saleHouse() {
        System.out.println("我是房东, 我出售房子");
    }
}

HouseProxy表示中介代理,同样实现 HouseSubject 接口。它内部持有一个 HouseSubject 类型的对象(即被代理的真实对象)

java 复制代码
// 3. 代理类(中介)
public class HouseProxy implements HouseSubject {
    private HouseSubject houseSubject;  // 被代理对象

    public HouseProxy(HouseSubject houseSubject) {
        this.houseSubject = houseSubject;
    }

    @Override
    public void rentHouse() {
        System.out.println("我是中介, 开始代理出租");
        houseSubject.rentHouse();
        System.out.println("我是中介, 代理出租结束");
    }

    @Override
    public void saleHouse() {
        System.out.println("我是中介, 开始代理出售");
        houseSubject.saleHouse();
        System.out.println("我是中介, 代理出售结束");
    }
}

客户端创建真实房东,创建中介代理,并将房东传入,最终的结果是通过代理租房/卖房。

java 复制代码
// 4. 客户端测试
public class Client {
    public static void main(String[] args) {
        HouseSubject landlord = new RealHouseSubject();
        HouseSubject proxy = new HouseProxy(landlord);

        proxy.rentHouse();
        proxy.saleHouse();
    }
}
动态代理

在上面静态代理的例子中,每次修改接口(Subject)和业务实现类(RealSubject)时, 还需要修改代理类(Proxy)。如果有新增接口(Subject)和业务实现类(RealSubject),也需要对每一个业务实现类新增代理类(Proxy)。

对于动态代理来说,不需要针对每个目标对象都单独创建一个代理对象,而是把这个创建代理对象的工作推迟到程序运行时,由JVM来实现。也就是说动态代理在程序运行时,根据需要动态创建生成。

Java也对动态代理提供了常见的API,

  • JDK 动态代理
  • CGLIB 动态代理

(1)JDK 动态代理的步骤如下

  1. 定义一个接口及其实现类(静态代理中的 HouseSubject 和 RealHouseSubject )
  2. 自定义 InvocationHandler 并重写 invoke 方法,在 invoke 方法中会调用目标方法(被代理类的方法)并自定义一些处理逻辑
  3. 通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[]interfaces,InvocationHandler h) 方法创建代理对象
java 复制代码
@Slf4j
public class JDKInvocationHandler implements InvocationHandler {
    //目标对象即就是被代理对象
    private Object target;

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

	/**
     * 调用目标方法,并对方法进行增强
     * @param proxy 代理类
     * @param method 目标方法
     * @param args 参数
     */
     
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 代理增强内容
        log.info("JDK动态代理开始");
        //通过反射调用被代理类的方法
        Object result= method.invoke(target, args);
        //代理增强内容
        log.info("JDK动态代理结束");
        return result;
    }
}
java 复制代码
public class Main {
	public static void main(String[] args) {
		// JDK 动态代理
		// 目标类
		HouseSubject target = new RealHouseSubject();
		//生成代理对象
		HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(
		        target.getClass().getClassLoader(),
		        new Class[]{HouseSubject.class},
		        new JDKInvocationHandler(target));
		
		proxy.rentHouse();
		proxy.saleHouse();
	}
}

Proxy 类中的 newProxyInstance 方法用于生成一个代理对象,参数Class<?>[] interfaces 决定了JDK动态代理只能代理实现了接口的⼀些类。

java 复制代码
public static Object newProxyInstance(ClassLoader loader,
    Class<?>[] interfaces,
    InvocationHandler h)
    throws IllegalArgumentException
{
    //...代码省略
}

(2)CGLIB 动态代理的实现步骤如下,

  1. 定义一个类(被代理类)
  2. 自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于增强目标方法,和 JDK 动态代理中的 invoke 方法类似
  3. 通过 Enhancer 类的 create()创建代理类
java 复制代码
@Slf4j
public class CGLIBInterceptor implements MethodInterceptor {
	//目标对象, 即被代理对象
	private Object target;
	
	public CGLIBInterceptor(Object target){
		this.target = target;
	}
	/**
	 * 调用目标方法,并对目标方法进行功能增强
	 *
	 * @param o            代理对象
	 * @param method       目标方法
	 * @param objects      参数
	 * @param methodProxy  方法代理
	 * @return 返回值
	 * @throws Throwable 异常
	 */
	@Override
	public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
		// 代理增强内容
		log.info("CGLib动态代理开始");
		//通过反射调用被代理类的方法
		Object result= methodProxy.invoke(target, objects);
		//代理增强内容
		log.info("CGLib动态代理开始");
		return result;
	}
}
java 复制代码
public class DynamicMain {
	public static void main(String[] args) {
		HouseSubject target = new RealHouseSubject();
		HouseSubject proxy = (HouseSubject) 
		    Enhancer.create(target.getClass(), new CGLIBInterceptor(target));
		proxy.rentHouse();
		proxy.saleHouse();
	}
}

Spring Framework 使用 JDK 代理和 CGLIB 代理,两种方式。如果代理的是接口就使用JDK代理,如果代理的是类(未实现接口)就使用CGLIB 代理。

可以通过@EnableAspectJAutoProxy(proxyTargetClass = true)来设置默认的代理方式,如果默认代理是JDK,那么处理接口时使用JDK代理,处理类(未实现接口)时使用CGLIB 代理;如果默认代理是CGLIB 代理,那么无论有没有实现接口,都是用CGLIB 代理;

Spring Boot 2.X之前, 默认使用 JDK 代理,此时的代理方式和Spring Framework一样;Spring Boot 2.X开始, 默认全部使用 CGLIB 代理;


Spring、Spring Boot 、Spring MVC区别

  • Spring:Spring是一个开发应用框架,有这么几个标签:轻量级、一站式、模块化,其目的是用于简化企业级应用程序开发。

    • Spring的主要功能:管理对象,以及对象之间的依赖关系, ⾯向切⾯编程, 数据库事务管理, 数据访问, web框架⽀持等.
    • 但是Spring具备高度可开放性, 并不强制依赖Spring, 开发者可以⾃由选择Spring的部分或者全部, Spring可以⽆缝继承第三⽅框架, ⽐如数据访问框架(Hibernate 、JPA), web框架(如Struts、JSF)
  • Spring MVC:Spring MVC是Spring的一个子框架,Spring诞生之后,许多人觉得很好用,于是按照MVC模式设计了一个 MVC框架(一些用Spring 解耦的组件),主要用于开发WEB应用和网络接口,所以,Spring MVC是一个Web框架。

    • Spring MVC基于Spring 进行开发的,天生的与Spring框架集成。可以让我们更简洁的进行Web层开发,⽀持灵活的 URL 到页面控制器的映射,提供了强大的约定大于配置的契约式编程支持, 非常容易与其他视图框架集成,如 Velocity、FreeMarker等
  • Spring Boot:Spring Boot是对Spring的一个封装,为了简化Spring应用的开发而出现的,中小型企业没有成本研究自己的框架,使用Spring Boot可以更加快速的搭建框架,降级开发成本,让开发人员更加专注于Spring应用的开发,而无需过多关注XML的配置和一些底层的实现。

    • Spring Boot 是个脚手架,插拔式搭建项目,可以快速的集成其他框架进来
    • 比如想使用SpringBoot开发Web项目,只需要引入Spring MVC框架即可, Web开发的工作是SpringMVC完成的,而不是SpringBoot,想完成数据访问, 只需要引入 Mybatis 框架即可。Spring Boot只是辅助简化项目开发的,让开发变得更加简单,甚至不需要额外的web服务器,直接生成 jar 包执行即可。
相关推荐
zhangren024682 小时前
Laravel8.x核心特性全解析
vue.js·spring boot·mysql
Java源码jdk2 小时前
基于javaweb和mysql的springboot校园二手书交易管理系统(java+springboot+vue+elementui+layui+mysql)
java·spring boot·mysql
sheji34162 小时前
【开题答辩全过程】以 校园帮系统为例,包含答辩的问题和答案
java·spring boot
小松加哲2 小时前
# Spring Aware 与 BeanPostProcessor:作用、使用与原理(源码级)
java·后端·spring
小松加哲3 小时前
Spring AOP 代理创建时机深度解析:初始化阶段 vs 三级缓存(源码级)
java·spring·缓存
摇滚侠3 小时前
SpringBoot yml 配置文件,读取 Windows 系统环境变量
windows·spring boot·后端
harder3213 小时前
Swift 面向协议编程的 RMP 模式
开发语言·ios·mvc·swift·策略模式
希望永不加班3 小时前
SpringBoot 跨域问题(CORS)彻底解决方案
java·spring boot·后端·spring