一、Spring的注解支持
1. 为什么使用Spring注解
简化UserServiceImpl注入UserMapper对象。
2. Spring支持的注解(IoC/DI相关)(重要)
下面@Repository、@Service、@Controller、@Configuration都是@Component注解的子注解,作用相同。
主要的区别是语义上的区别。当看到不同的注解放在不同层的类中。但是不按照语义去做,非把@Service用在持久层,也是有效果的。但是这样却是不规范的。
注解名称 | 解释 |
---|---|
@Component | 实例化Bean,默认名称为类名首字母变小写(类名不要出现类似AServiceImpl)。支持自定义名称 |
@Repository | @Component子标签,作用和@Component一样,用在持久层。 |
@Service | @Component子标签,作用和@Component一样,用在业务层。 |
@Controller | @Component子标签,作用和@Component一样,用在控制器层。 |
@Configuration | @Component子标签,作用和@Component一样,用在配置类。 |
@Autowired | 自动注入。默认byType,如果多个同类型bean,使用byName(默认通过属性名查找是否有同名的bean,也可以通过@Qualifier("bean名称"),执行需要注入的Bean名称) |
@Resource | 非Spring注解。默认byName,如果没找到,使用byType。 |
3. 如何使用注解
需要在spring.xml配置文件中添加context命名空间(每个命名空间需要添加三行)。
设置扫描包中的注解,完成对象创建及属性注入。如果需要扫描多个包,包路径之间使用逗号分隔。
<!-- 多个包使用逗号分隔,下面也可以只写com.bjsxt -->
<context:component-scan base-package="com.bjsxt.service,com.bjsxt.mapper"/>
@Component注解及子注解使用时,如果没有明确指定bean的名称,默认名称为类名首字母变小写。自定义id:@Service("id值")
//@Service使用使用示例
@Service
public class PeopleServiceImpl implements PeopleService { //容器中存储 peopleServiceImpl
@Autowired //属性值自动注入,先byType后byName
private PeopleMapperImpl peopleMapper;
@Override
public void test() {
System.out.println(peopleMapper);
}
}
4 .@Autowired自动注入
自动注入:spring管理的bean与bean之间的注入。
方式: 1.xml的bean标签中配置属性的自动注入(依赖于set方法) 2.注解
4.1 如何注入
先byType自动注入,出现多个相同的类型,再使用byName自动注入。配合@Qualifier指定注入bean的id。默认的id为:userServiceImpl
4.2 自动注入依赖的方法
1.@Autowired用在属性上
重点:没有提供自动注入属性的有参构造方法,底层使用反射完成自动注入。
了解:提供了自动注入属性的有参构造方法没有提供无参构造方法,底层使用有参构造完成自动注入。
2. @Autowired用在set方法上
使用设值完成自动注入(使用set方法完成注入)
3. @Autowired用在有参构造方法上
使用构造完成自动注入(使用有参构造方法完成注入)其他:根据名字注入,同时使用@Autowired和@Qualifier("beanName")
二、Spring Test 模块
1. 介绍
Spring Test 模块整合了一些常见的单元测试工具,例如Junit。
整合后可以在测试类中直接使用Spring容器中的内容,把测试类也放入到Spring容器中,测试类里面可以直接使用注解注入容器中的bean对象。
同时也可以通过@ContextConfigration注解指定配置文件路径,让测试方法在启动的时候直接加载配置文件。
2. 添加依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.16</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.16</version>
<scope>test</scope>
</dependency>
</dependencies>
3. 编写测试
// 使用Spring整合Junit4的类启动当前测试类
@RunWith(SpringJUnit4ClassRunner.class)
// 启动时加载的配置文件,里面要包含classpath
@ContextConfiguration(locations = "classpath:applicatonContext.xml")
public class MyTest {
@Autowired
PeopleService peopleService;
@Test
public void test2(){
peopleService.test();
}
}
三、代理设计模式(重点)
1. 介绍
代理模式是Java常见的设计模式之一。所谓代理模式是指客户端并不直接调用实际的对象,而是通过调用代理,来间接的调用实际的对象。
代理设计模式包括:静态代理和动态代理。
静态代理:代理对象由程序员自己编写。
动态代理: 代理对象是动态产生的,动态代理又分为JDK动态代理和Cglib动态代理两种实现方式。其中JDK动态代理是基于接口实现,也就是代理对象和真实对象需要实现相同的接口,JDK动态代理是Java官方提供的技术。Cglib动态代理是基于继承实现的(也可以基于接口实现),也就是代理对象需要继承真实对象,Cglib动态代理是第三方的技术,使用的时候需要导入jar包。
2. 动态代理(重点)
2.1 JDK动态代理
核心代码:Proxy.newProxyInstance();
2.1.1 使用
JDK动态代理是基于接口,被代理的类必须实现了接口。首先我们需要创建接口。
- 先创建接口
- 然后创建被代理对象类
- 测试类
//JDK动态代理
/*
test01.JDK动态代理为类创建代理对象,实现目标对象功能的增强
test02.JDK动态代理为接口创建实现类
*/
@Test
/*
参数一:类加载
参数二:接口的类对象(JDK动态代理基于接口实现)
参数三:运行时底层实现select()方法【该过程看不到】,实现的方法中调用了 invoke()
*/
public void test01() {
//创建被代理对象
UserServicesImpl userServicesImpl = new UserServicesImpl();
UserService userService = (UserService) Proxy.newProxyInstance(
Test2.class.getClassLoader(),//
UserServicesImpl.class.getInterfaces(),//必须为接口的类对象
new InvocationHandler() {//
@Override
/*
* 参数一:代理对象
* 参数二:目标方法(被代理方法)
* 参数三:目标方法参数
* */
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
Object invoke = method.invoke(userServicesImpl, objects);
return invoke;
}
}
);
//代理对象调用方法
userService.select();
}
2.1.2 作用和参数含义
2.2 Cglib动态代理
Cglig动态是第三方提供的技术,需要导入jar包,并且可以是基于类继承,也可以是基于接口实现。
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
2.1.1 使用
1.先创建接口或类
2.然后创建被代理的对象类(继承或实现)
3.测试类(基于接口实现):
@Test//2.为类创建代理对象,实现目标方法的功能增强(基于接口实现)
public void test04(){
//创建目标对象(被代理对象)
UserServicesImpl userServicesImpl = new UserServicesImpl();
Enhancer enhancer = new Enhancer();
enhancer.setInterfaces(UserServicesImpl.class.getInterfaces());
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("前置 增强");
//调用 目标方法
Object invoke = method.invoke(userServicesImpl, objects);
System.out.println("后置 增强");
return invoke;
}
});
//获取代理对象并调用方法
UserService userService = (UserService) enhancer.create();
userService.select();
}
4.测试类(基于类继承):
@Test// 3.为类创建代理对象,实现目标方法的功能增强(基于继承实现)
public void test05(){
Enhancer enhancer = new Enhancer();//增强器
enhancer.setSuperclass(Aaa.class);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("前置 增强");
Object invoke = methodProxy.invokeSuper(o, objects);
System.out.println("后置 增强");
return invoke;
}
});
//获取代理对象调用方法
UserServicesImpl2 userServices = (UserServicesImpl2) enhancer.create();
userServices.querry(1,2);
}
}
2.2.2 作用和参数含义
2.3 动态代理总结
JDK动态代理机制是委托机制,只能对实现了接口的类生成代理,底层通过反射机制实现。(重点)
CGLIB动态代理机制是接口或继承机制,针对类生成代理,被代理类和代理类是继承关系,底层通过字节码处理框架asm,修改字节码生成子类
四、SpringAOP介绍(重点)
1. SpringAOP介绍(常见面试题)
1.1. 介绍
- 面向切面编程 (AOP) 通过提供另一种思考程序结构的方式来补充面向对象编程 (OOP)。OOP 中模块化的关键单位是类,而 AOP 中模块化的单位是切面。切面能够实现跨越多种类型和对象的关注点(例如事务管理)的模块化。
- Spring 的关键组件之一是 AOP 框架。虽然 Spring IoC 容器不依赖于 AOP(意味着如果您不想使用 AOP,则不需要使用 AOP),但 AOP 补充了 Spring IoC 以提供一个非常强大的中间件解决方案。
总结:
AOP 叫做面向切面编程。
AOP 是对OOP的补充。
AOP的核心是切面。
AOP是对IoC的补充。
1.2. AOP中的专业术语()
Aspect(切面) | 为方法添加增强功能的过程。 |
join point(切入点 ) | 被代理的方法(目标方法)。 |
Advice (通知 ) | 增强内容,添加的拓展。 |
Pointcut(切点 ) | 表达式(通过切点表达式可以找到需要增强功能的方法),通过表达式说明哪些方法是切入点。 |
AOP Proxy(代理 ) | Spring支持JDK动态代理(默认)和cglib动态代理两种方式,可以通过proxy-target-class=true把默认的JDK动态代理修改为Cglib动态代理。 |
Weaving(织入) | 织入就是把 通知 添加到 切入点 的过程。 |
在实际开发中AOP主要应用在Service层。
1.3 什么是AOP(面试问题)
AOP叫做面向切面编程,属于对OOP的扩展。其实现是基于动态代理设计模式,在IoC(控制反转)基础上实现的。
AOP就是对某个切入点做了通知进行增强扩展,形成横切面。可以实现在不修改原有代码的情况下,做额外扩展。
2. 实现AOP的两种方式(重点)
在Spring中提供了两种方式实现AOP:
Schema-based:所有的通知都需要实现特定类型的接口实现通知。
AspectJ:可以使用普通Java类结合特定的配置实现通知。
3. AOP底层代理模式
SpringAOP底层默认使用的JDK动态代理,但是同时也支持cglib动态代理。
需要配置Cglib的依赖以及在Spring的配置文件中开启Cglib动态代理。
五、Schema-based方式实现AOP
1. 在Schema-based方式中通知的分类(面试题)
前置通知:在切入点之前执行的增强功能。通知需要实现MethodBeforeAdvice接口。
后置通知:在切入点之后执行的增强功能。通知需要实现AfterReturningAdvice接口。
环绕通知:一个方法包含了前置通知和后置通知的功能。通知需要MethodInterceptor接口。
异常通知:如果切入点中出现了异常(绝对不能try...catch解决了异常)就会触发异常通知。通知需要实现ThrowsAdvice接口。
添加新的依赖:
<!--支持切点表达式等-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.9.1</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
2.创建通知类
前置通知类:
/*
* MethodBeforeAdvice接口中必须重写before方法,方法中三个参数:
* method:切入点方法对象。
* args:切入点方法参数。
* target:切入点方法所在的对象。
*/
package com.bjsxt.advice;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
public class MyBefore implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("前置通知");
}
}
后置通知类:
/*
* AfterReturningAdvice接口中必须重写afterReturning方法,方法中四个参数:
* returnValue:返回值
* method:切入点方法对象
* args:切入点方法参数
* target:切入点方法所在的对象
* */
package com.bjsxt.advice;
import org.springframework.aop.AfterReturningAdvice;
import java.lang.reflect.Method;
public class MyAfter implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("后置通知");
}
}
环绕通知类:
/*
* MethodInterceptor接口中必须重写invoke方法,方法中参数:
* invocation:方法调用器。通过invocation可以proceed()方法调用执行点。
* */
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class MyAround implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("环绕通知-前置增强");
Object result = invocation.proceed();// 执行切入点
System.out.println("环绕通知-后置增强");
return result;
}
}
异常通知类:
/*
* ThrowsAdvice接口没有方法,但是我们必须严格提供一个下面的方法:
* public void afterThrowing:必须相同
* 必须有Exception参数
* 也就是说,虽然ThrowsAdvice虽然没有方法,但是我们必须自己写一个和下面一样的方法。
* */
import org.springframework.aop.ThrowsAdvice;
public class MyThrow implements ThrowsAdvice {
public void afterThrowing(Exception e){
System.out.println("异常通知:"+e.getMessage());
}
}
3. 配置切面
<!-- 配置注解扫描路径 -->
<context:component-scan base-package="com.bjsxt"/>
<!-- 配置前置通知对象 -->
<bean id="mybefore" class="com.bjsxt.advice.MyBefore"/>
<!-- 配置后置通知对象 -->
<bean id="myafter" class="com.bjsxt.advice.MyAfter"/>
<!-- 配置环绕通知对象 -->
<bean id="myaround" class="com.bjsxt.advice.MyAround"/>
<!-- 配置异常通知对象 -->
<bean id="mythrow" class="com.bjsxt.advice.MyThrow"/>
<aop:config>
<aop:pointcut id="mypoint" expression="execution(* com.bjsxt.service.impl.PeopleServiceImpl.test())"/>
<!-- 织入异常通知 -->
<aop:advisor advice-ref="mythrow" pointcut-ref="mypoint"/>
</aop:config>
4. 多个通知的执行顺序
如果切面中包含多个通知,执行顺序是按照配置顺序执行。
前置通知:先配置的先执行。
后置通知:先配置的后执行。
六、AspectJ实现AOP(重点)
1. AspectJ方式通知类型(面试题)
前置通知:before。
后置通知:after。
after是否出现异常都执行的后置通知。
after-returning切入点不出现异常时才执行的后置通知。
环绕通知:around。
异常通知:after-throwing。
2. 代码实现
2.1 创建通知类
Aspectj方式实现AOP的通知类不需要实现任何的接口,直接声明一个普通java类即可,然后在类中直接定义通知方法即可,方法名随意。
import org.aspectj.lang.ProceedingJoinPoint;
public class MyAdvice {
//前置通知方法
public void before(){
System.out.println("我是前置通知方法...");
}
//后置通知方法
public void after(){
System.out.println("我是后置通知方法...");
}
//环绕通知方法
public Object round(ProceedingJoinPoint pp) throws Throwable {
System.out.println("环绕---前");
//放行
Object proceed = pp.proceed();
System.out.println("环绕---后");
return proceed;
}
//异常通知方法
public void myThrow(Exception ex){
System.out.println("我是异常通知......"+ex.getMessage());
}
}
2.2 配置AOP
在spring.xml中配置
<!-- 配置注解扫描路径 -->
<context:component-scan base-package="com.bjsxt"/>
<!-- 创建通知对象 -->
<bean id="myadvice" class="com.bjsxt.advice.MyAdvice"/>
<aop:config>
<!-- 基于Aspectj方式配置 -->
<aop:aspect ref="myadvice">
<!-- 切点配置 -->
<aop:pointcut id="mp" expression="execution(* com.bjsxt.service.*.*(..))"/>
<!-- 配置前置通知 -->
<aop:before method="before" pointcut-ref="mp"/>
<!-- 配置后置通知(没有异常,执行的后置通知 )-->
<aop:after-returning method="after" pointcut-ref="mp"/>
<!-- 配置后置通知(是否出现异常,都会执行的后置通知) -->
<aop:after method="after" pointcut-ref="mp"/>
<!-- 配置异常通知 throwing="异常参数名"-->
<aop:after-throwing method="myThrow" pointcut-ref="mp" throwing="ex"/>
</aop:aspect>
</aop:config>
注意:
after和after-returning,after无论切点是否出现异常都执行的后置通知,after-returning只有在切点正常执行完成,才会触发的通知。
3. 在通知中获取目标方法的参数
通知配置中不能有参和无参混用,要么通知方法全部有参,要么全部无参!
在配置文件中配置:
<!-- 配置注解扫描路径 -->
<context:component-scan base-package="com.bjsxt"/>
<!-- 配置通知对象 -->
<bean id="myadvice2" class="com.bjsxt.advice.MyAdvice2"></bean>
<aop:config>
<!-- 基于Aspectj方式配置 -->
<aop:aspect ref="myadvice2">
<!--
切点配置
args():编写参数名,参数名称和目标方法中参数名称一致。
-->
<aop:pointcut id="mypoint" expression="execution(* com.bjsxt.service.impl.PeopleServiceImpl.test(int,boolean)) and args(id1,bool1)"/>
<aop:before method="mybefore" pointcut-ref="mypoint" arg-names="id1,bool1"/>
<aop:after-returning method="myafter" pointcut-ref="mypoint" arg-names="id1,bool1"/>
<aop:after method="myafter2" pointcut-ref="mypoint" arg-names="id1,bool1"/>
<!-- pjp由Spring自动注入 -->
<aop:around method="myaround" pointcut-ref="mypoint" arg-names="pjp,id1,bool1"/>
<aop:after-throwing method="mythrow" pointcut-ref="mypoint" arg-names="ex,id1,bool1" throwing="ex"/>
</aop:aspect>
</aop:config>
通知类:
import org.aspectj.lang.ProceedingJoinPoint;
/*
如果希望获取切入点方法的参数,推荐把通知的参数和切入点的参数写成一致。
*/
public class MyAdvice2 {
public void mybefore(int id1, boolean bool1){
System.out.println("前置通知"+id1+","+bool1);
}
public void myafter(int id1, boolean bool1){
System.out.println("后置:"+id1+","+bool1);
}
public void myafter2(int id1, boolean bool1){
System.out.println("后置2:"+id1+","+bool1);
}
public Object myaround(ProceedingJoinPoint pjp,int id1, boolean bool1) throws Throwable {
System.out.println("环绕前置");
Object result = pjp.proceed();
System.out.println("环绕后置");
return result;
}
public void mythrow(Exception ex,int id1, boolean bool1){
System.out.println("异常通知:"+ex.getMessage()+",id:"+id1+",bool1:"+bool1);
}
}
4. Schema-based和Aspectj的区别
- Schema-based:基于接口实现的。每个通知都需要实现特定的接口类型,才能确定通知的类型。由于类已经实现了接口,所以配置起来相对比较简单。尤其是不需要在配置中指定参数和返回值类型。
- AspectJ方式:是基于配置实现的。通过不同的配置标签告诉Spring通知的类型。AspectJ方式对于通知类写起来比较简单。但是在配置文件中参数和返回值需要特殊进行配置。
- 因为Schame-based是运行时增强,AspectJ是编译时增强。所以当切面比较少时,性能没有太多区别。但是当切面比较多时,最好选择AspectJ方式,因为AspectJ方式要快很多。
七、注解方式实现AOP
1. 注解实现的配置
注解方式只支持替换AspectJ的XML配置。
<!--配置注解扫描路径-->
<context:component-scan base-package="com.bjsxt"/>
<!--配置AOP注解生效-->
<aop:aspectj-autoproxy expose-proxy="true"/>
注意:
配置Spring容器注解扫描的路径。
配置AOP注解生效。
2. 注解类型
2.1 @Component
- 相当于配置文件的bean标签,将某个类的对象扫描到Spring容器中。此注解一般在普通Java类上用。
- 默认类名的首字母小写即为bean对象的ID,也可以使用注解的value属性声明自定义的ID,value可以省略不写。
- 声明在类上。
@Component("advice")
public class MyAdvice {
}
@Service 也属于@Component的一种,用在业务层(Service层)
2.2 通知类中的注解
1. @Aspect
声明该类为通知类。结合@Component在通知类上使用,Spring扫描到容器中并作为通知类。
@Component("advice")
@Aspect
public class MyAdvice {
}
2.@pointcut
声明切点,方法上使用 。
@Component("advice")
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.bjsxt.service.*.*(..))")
public void p(){}
}
3.@Before
声明方法为前置通知方法。
//前置通知,p()为@pointcut注解的方法
@Before("p()")
public void before(){
System.out.println("我是前置通知");
}
4. @After
声明方法为后置通知方法。
//后置通知,p()为@pointcut注解的方法
@AfterReturning("p()")
public void after(){
System.out.println("我是后置通知");
}
5.@Around
声明方法为环绕通知方法。
//p()为@pointcut注解的方法
@Around("p()")
public Object round(ProceedingJoinPoint pp) throws Throwable {
System.out.println("环绕-前");
Object proceed = pp.proceed();
System.out.println("环绕-后");
return proceed;
}
6.@AfterThrowing
声明方法为异常通知方法
//p()为@pointcut注解的方法
@AfterThrowing(value = "p()",throwing = "e")
public void myThrow(Exception e){
System.out.println("我是异常通知");
}
注意:
需要在通知的注解中声明对应的切点的路径,非同包中需要指定全限定路径。