Spring中的核心概念
控制反转(IoC)与依赖注入(DI)
XML文件配置
- id名字不能乱取,对应接口中的方法名
- class表示是接口对应的类,这种更加不能乱取了,而且要讲模块名完整加进来
引用类型的就用ref
总结一下依赖注入的基本流程
要注意name中bookDao和ref中bookDao不是同一个东西
- 背景介绍 - 在Spring框架中,
<property>标签通常用于配置JavaBean的属性。
当你看到
<property name = "bookDao"/>这样的配置,它是在为一个包含bookDao属性的JavaBean进行属性注入的配置。
- 详细解释
- name属性内容:
-
name里面配置的是JavaBean中的属性名称。在这个例子中,bookDao是一个属性名。
-
- 这个属性通常是在对应的Java类(一般是一个Spring管理的Bean)中定义的。例如,假设有一个名为
BookService的类,它可能有一个如下的属性:
- 这个属性通常是在对应的Java类(一般是一个Spring管理的Bean)中定义的。例如,假设有一个名为
private BookDao bookDao;
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao; }
- Spring会通过反射机制,在创建
BookService这个Bean的时候,根据<property>标签中的name属性(这里是bookDao)找到对应的setBookDao方法。 - 然后,Spring会尝试将一个
BookDao类型的对象注入到这个属性中。
3.配置目的:这种配置方式实现了依赖注入。
- 通过将
bookDao属性注入到包含这个属性的Bean(如BookService)中,使得这个Bean可以使用bookDao对象提供的方法来完成业务逻辑。例如,BookService可能会使用bookDao的findById
public Book getBookById(Long id) {
return bookDao.findById(id); }
bean基础配置
bean的作用范围
bean实例化的三种方法
其实后面用SpringBoot做开发的时候,基本是用注解@Autowired注解在bean对象上面的了,下面的东西不用太在意
构造方法(常用)
静态工厂(了解)
实例工厂(了解)
alt+insert创建构造方法
查看接口的层级,选中接口->Navigator->Hierachy
bean的生命周期
setter注入
引用类型
简单类型
构造器注入
type属性和index属性
自动装配
把原来的<property name = "bookDao" ref = "bookDao" />注释掉
注意,自动装配不能对简单类型操作
数据源对象管理
方式二中,相对路径不行的话,可以使用绝对路径
获取Bean
注解开发定义Bean
@Component中如果没有指定名称,需要通过指定类名来传递,如下面的BookService.class
再来看一个例子
@Component("fooFormatter")
public class FooFormatter implements Formatter {
public String format() {
return "foo";
}
}
@Component("barFormatter")
public class BarFormatter implements Formatter {
public String format() {
return "bar";
}
}
@Component
public class FooService {
@Autowired
private Formatter formatter;
//todo
}
上面的例子会抛出异常NoUniqueBeanDefinitionException,应该加上@Qualifier,如下
@Component
public class FooService {
@Autowired
@Qualifier("fooFormatter")
private Formatter formatter;
//todo
}
或者也可以在Formatter实现类上使用@Qualifier注释,而不是在@Component 或者@Bean 中指定名称,也能达到相同的效果
@Component
@Qualifier("fooFormatter")
public class FooFormatter implements Formatter {
public String format() {
return "foo";
}
}
@Component
@Qualifier("barFormatter")
public class BarFormatter implements Formatter {
public String format() {
return "bar";
}
}
一些常用的衍生注解
纯注解开发
1. @Service 注解
@Service 是Spring框架提供的一个注解,用于标记类为业务逻辑层的组件。当类上标注了@Service注解后,Spring容器会自动扫描并创建该类的一个实例(即Bean),这样我们就可以在其他地方通过自动装配(Autowired)的方式注入这个Bean。
示例代码: 假设我们有一个用户服务类UserService,它包含了一些与用户相关的业务逻辑方法。
import org.springframework.stereotype.Service;
@Service
public class UserService {
// 假设有一个userRepository用于数据库操作
private final UserRepository userRepository;
// 通过构造器注入UserRepository
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User findUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User with id " + id + " not found"));
}
// 其他业务逻辑方法...
}
在上面的代码中,UserService类上标注了@Service注解,这意味着Spring容器会管理它的生命周期,并且我们可以在其他地方通过@Autowired来注入这个服务。
2. @Mapper 注解(通常与MyBatis一起使用)
@Mapper注解通常不是Spring框架的一部分,而是MyBatis框架提供的。在Spring Boot项目中,如果集成了MyBatis,那么@Mapper注解用于标记接口,使得接口可以被MyBatis扫描到并生成对应的代理实现类。这样我们就可以通过这个接口来调用数据库的操作。
示例代码: 假设我们有一个用户映射器接口UserMapper,它定义了与数据库交互的方法。
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.Optional;
@Mapper
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
Optional<User> findById(Long id);
// 其他数据库操作方法...
}
在上面的代码中,UserMapper接口上标注了@Mapper注解,并且方法上使用了MyBatis提供的注解如@Select来定义SQL查询。这样,MyBatis会为这个接口生成一个实现类,我们可以在服务类中注入这个映射器接口来调用数据库操作。
注意:在某些配置中,如果你已经在启动类或配置类上使用了@MapperScan注解来指定扫描的包路径,那么映射器接口上的@Mapper注解可以省略。例如:
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.example.demo.mapper") // 指定扫描的包路径
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
在这个例子中,所有位于com.example.demo.mapper包下的接口都会被MyBatis扫描并处理,无需在每个接口上单独使用@Mapper注解
备注: @Resource的作用相当于@Autowired,只不过@Autowired按byType自动注入,而@Resource默认按 byName自动注入罢了。@Resource有两个属性是比较重要的,分是name和type。
两种加载初始化容器
- 跟上面的init-method和destroy-method有点像
- 下面的@Scope("singleton")指的是bean单例
- 改成@Scope("propertype")就是多例
注意看小字部分
值注入
注解开发管理第三方bean
使用外部文件properties加载,用PropertySource注入
不支持使用通配符,不可以写成classpath*:jdbc.properties,只能是classpath:jdbc.properties。
注解开发实现第三方bean注入资源
一般会使用独立的配置类管理第三方bean
Import版本
扫描版本
如果注释掉注解,那将会检测不到需要注入的资源而报错
注解开发总结
Spring整合MyBatis
- 第一步:导入相关坐标,版本问题可以看看网上怎么配置的,配置坐标的文件在pom.xml
-
第二步:在需要注入的方法上面标上Bean标签
-
第三步:黄色部分是包,蓝色部分是数据源,这部分在另外的配置文件xml里面
-
第四步:还有映射文件的部分,Mapper.XML里面有SQL语句
Spring整合JUnit
-
下面划红线的部分必须是
classes,跟以往不同,以往是value,这里如果不加classes =会报错 -
使用Spring整合Junit专用的类加载器
-
- @RunWith
-
- ContextConfiguration
-
@ContextConfiguration这个注解通常与@RunWith(SpringJUnit4ClassRunner.class)联合使用用来测试
AOP面向切面开发
核心概念
基本流程
- 注解
@Pointcut标注好切入点是哪个方法 - 注解
@Before标注好method方法在切入点之前执行 - 定义通知类有两个重要的标签
AOP开发流程总结
- 第一步
- 第二步
- 第三步
- 第四步
- 第五步,注意红色字的部分
- 第六步:
@Component定义通知类受Spring容器管理,@Aspect定义当前类为切面类 - 第七步:
@Configuration定义该类为配置类,@EnableAspectJAutoProxy开启Spring对AOP注解驱动支持
AOP相关包的导入
如果导入了spring-context,aop默认自动导入,无需额外导入。
通过打印Bean对象的类,得到,AOP方式开发是用代理的方式
AOP切入点表达式
@Around括号里面写类的方法,有两种形式,一种是只写方法,另外一种是类.方法名()。
下面说的原始方法指的时方法pt()里面的内容
- 当打印时,不知道打哪种方法时,用
Signature
AOP获取数据(参数、返回值、异常信息)
目前我们写AOP仅仅是在原始方法前后追加一些操作,接下来我们要说说AOP中数据相关的内容,我们将从获取参数、获取返回值和获取异常三个方面来研究切入点的相关信息。 前面我们介绍通知类型的时候总共讲了五种,那么对于这五种类型都会有参数,返回值和异常吗?
我们先来逐一分析下:
-
获取切入点方法的参数,所有的通知类型都可以获取参数
-
JoinPoint:适用于前置、后置、返回后、抛出异常后通知
-
ProceedingJoinPoint:适用于环绕通知
-
获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究
-
- 返回后通知
-
- 环绕通知
-
获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究
-
- 抛出异常后通知
-
- 环绕通知
获取参数
非环绕通知获取方式
在方法上添加JoinPoint,通过JoinPoint来获取参数
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.yolo.dao.BookDao.findName(..))")
public void pt(){}
@Before("pt()")
public void before(JoinPoint jp){
Object[] args = jp.getArgs();
System.out.println(Arrays.toString(args));
System.out.println("before advice ...");
}
}
运行App类,可以获取如下内容,说明参数100已经被获取
思考:方法的参数只有一个,为什么获取的是一个数组?
- 因为参数的个数是不固定的,所以使用数组更通配些。
- 如果将参数改成两个会是什么效果呢?
修改BookDao和BookDaoImpl类
public interface BookDao {
String findName(int id, String password);
}
@Repository
public class BookDaoImpl implements BookDao {
@Override
public String findName(int id, String password) {
System.out.println("id:" + id);
//返回一个固定值
return "TestName";
}
}
修改App类,调用方法传入多个参数
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, "rick666");
System.out.println(name);
}
}
输出结果如下,两个参数都已经被获取到
@After与@Before获取参数的方式相同,不再赘述
环绕通知获取方式
环绕通知使用的是ProceedingJoinPoint,因为ProceedingJoinPoint是JoinPoint类的子类,所以对于ProceedingJoinPoint类中应该也会有对应的getArgs()方法,我们去验证下
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.yolo.dao.BookDao.findName(..))")
public void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
Object res = pjp.proceed();
return res;
}
}
运行App后查看运行结果,说明ProceedingJoinPoint也是可以通过getArgs()获取参数
注意:
pjp.proceed()方法是有两个构造方法,分别是: proceed()
proceed(Object[] object)
proceed相关源码
Object proceed() throws Throwable;
Object proceed(Object[] var1) throws Throwable;
- 调用无参数的proceed,当原始方法有参数,会在调用的过程中自动传入参数
- 所以调用这两个方法的任意一个都可以完成功能(且运行结果一致)
- 但是当需要修改原始方法的参数时,就只能采用带有参数的方法,如下
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.yolo.dao.BookDao.findName(..))")
public void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
//对参数值进行修改
args[0] = 777;
Object res = pjp.proceed(args);
return res;
}
}
运行程序,输出结果如下
获取返回值
对于返回值,只有返回后AfterReturing和环绕Around这两个通知类型可以获取,具体如何获取?
环绕通知获取返回值
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.yolo.dao.BookDao.findName(..))")
public void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
//对参数值进行修改
args[0] = 777;
Object res = pjp.proceed(args);
return res;
}
}
上述代码中,res就是方法的返回值,我们是可以直接获取,不但可以获取,如果需要还可以进行修改。
返回后通知获取返回值
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.yolo.dao.BookDao.findName(..))")
public void pt(){}
@AfterReturning(value = "pt()", returning = "res")
public void afterReturning(Object res){
System.out.println("res = " + res);
System.out.println("afterReturning advice ...");
}
}
运行程序,输出如下,成功获取了返回值
几点注意:
参数名的问题
- 赋给returning的值,必须与Object类型参数名一致,上面的代码中均为res
- afterReturning方法参数类型的问题
- 参数类型可以写成String,但是为了能匹配更多的参数类型,建议写成Object类型
- afterReturning方法参数的顺序问题
- 如果存在JoinPoint参数,则必须将其放在第一位,否则运行将报错
获取异常
对于获取抛出的异常,只有抛出异常后AfterThrowing和环绕Around这两个通知类型可以获取,具体如何获取?
环绕通知获取异常
这块比较简单,以前我们是抛出异常,现在只需要将异常捕获,就可以获取到原始方法的异常信息了
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) {
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
//对参数值进行修改
args[0] = 777;
Object res = null;
try {
res = pjp.proceed(args);
} catch (Throwable e) {
e.printStackTrace();
}
return res;
}
在catch方法中就可以获取到异常,至于获取到异常以后该如何处理,这个就和你的业务需求有关了。
抛出异常后通知获取异常
@AfterThrowing(value = "pt()", throwing = "t")
public void afterThrowing(Throwable t){
System.out.println("afterThrowing advice ..." + t);
}
那现在我们只需要让原始方法抛一个异常来看看效果
@Repository
public class BookDaoImpl implements BookDao {
@Override
public String findName(int id, String password) {
System.out.println("id:" + id);
//模拟异常
int i = 1/0;
//返回一个固定值
return "TestName";
}
}
运行程序,输出如下,成功输出了异常