若有错,请指出
第二章 搭建Springboot环境,配置视图解析器jsp页面
第三章 全注解下的Spring Ioc
第四章 约定编程-Spring AOP
第五章 Spring Boot的数据库编程
文章目录
4.1 动态代理
将通用的代码抽取出来简化开发流程
新建项目模拟吃饭流程
4.1.1 概念
动态代理的意义在于生成一个占位(又称代理对象)来代理目标对象,从而增强或者控制真实对象的访问。主要包括JDK和CGLIB 这两种常用的
- 存在接口的时候默认使用JDK动态代理,否则CGLIB代理
- JDK动态代理要求类存在接口,CGLIB不需要
- 推荐使用接口的方式编程,定义(接口)与实现(实现类)相分离具有更好的扩展性
在这副图中假设这样一个场景,记者想要采访儿童问题,儿童(目标对象)无法准确回答,那么父母(代理对象)就可以帮助回答。
又比如房客,中介,房东中,房客想要与房东(目标对象)签订合同可通过中介(代理对象)执行签订。
4.1.2 JDK动态代理实现步骤
1.建立目标对象与代理对象的关联关系
2.实现代理对象的逻辑方法(InvocationHandler接口的invoke方法)
目录结构
新建接口吃饭接口
public interface DinnerService {
public void have(String desc, Date date);
}
吃饭实现类(目标对象)
//目标对象
@Service
public class LunchServiceImpl implements DinnerService {
@Override
public void have(String desc, Date date) {
System.out.println("吃午饭,吃的是【"+desc+"】,时间是【"+date+"】");
//throw new RuntimeException("发生了急事");//模拟发生急事
}
}
代理类实现动态代理逻辑
//动态代理:代理对象实现InvocationHandler接口的invoke方法
public class DinnerProxy implements InvocationHandler {
//目标对象,房东
private Object target=null;
//1.建立代理对象与目标对象的关联关系(中介和房东签署合同)
public static Object bind(Object target){
DinnerProxy dinnerProxy=new DinnerProxy();
dinnerProxy.target=target;
//生成代理对象
Object proxy= Proxy.newProxyInstance(
//类加载器
target.getClass().getClassLoader(),
//绑定的接口,把代理对象绑定到哪些接口下,可以是多个
target.getClass().getInterfaces(),
dinnerProxy
);
return proxy;
}
/**
*
* @param proxy 代理对象
* @param method 方法
* @param args 参数
* @return 方法调用结果
* @throws Throwable 异常
*/
//2.代理的逻辑方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("洗手");
//吃饭,代理对象执行目标对象逻辑方法的方法
Object obj=method.invoke(target,args);
System.out.println("收拾饭菜");
System.out.println("收拾餐具");
return obj;
}
}
创建主类
//测试动态代理执行类
public class AopMain {
public static void main(String[] args){
//新建目标对象
DinnerService target=new LunchServiceImpl();
//绑定目标对象,
DinnerService proxy=(DinnerService) DinnerProxy.bind(target);
//执行代理对象的invoke方法代替目标对象实现have方法
proxy.have();
}
}
为什么代理对象这里能直接强制转换到目标对象的接口呢?
DinnerService proxy=(DinnerService) DinnerProxy.bind(target);
原因是在第一步中,第二个参数中下挂到目标对象的接口
运行截图
4.2 约定编程
4.2.1 案例
模拟吃饭发生急事
吃饭这个过程就是目标对象的方法,其他过程如洗手之类的过程则抽离出来,实现逻辑过程写入拦截器类DinnerInterceptor实现类中,简化DinnerProxy的invoke方法代码量
目录结构
新增DinnerInterceptor接口实现类DinnerInterceptorImpl
public class DinnerInterceptorImpl implements DinnerInterceptor {
@Override
public void beforeDinner() {
System.out.println("饭前洗手");
}
@Override
public void afterDinner() {
System.out.println("收拾餐具");
}
@Override
public void afterThrowing() {
System.out.println("处理急事");
}
@Override
public void after() {
System.out.println("处理饭菜");
}
}
修改DinnerProxy类
//动态代理:代理对象实现InvocationHandler接口的invoke方法
//该类实现将服务类和拦截方法织入对应流程
public class DinnerProxy implements InvocationHandler {
//目标对象,房东
private Object target=null;
//拦截器
private DinnerInterceptor dinnerInterceptor=null;
//1.建立代理对象与目标对象的关联关系(中介和房东签署合同)
public static Object bind(Object target,DinnerInterceptor dinnerInterceptor){
//1.创建DinnerProxy對象
DinnerProxy dinnerProxy= new DinnerProxy();
//2.DinnerProxy對象賦值目標對象值
dinnerProxy.target=target;
//3.DinnerProxy對象赋值dinnerInterceptor
dinnerProxy.dinnerInterceptor=dinnerInterceptor;
//4.生成代理对象
Object proxy= Proxy.newProxyInstance(
//类加载器
target.getClass().getClassLoader(),
//绑定的接口,把代理对象绑定到哪些接口下,可以是多个
target.getClass().getInterfaces(),
//绑定代理对象实现逻辑
dinnerProxy
);
return proxy;
}
/**
*
* @param proxy 代理对象
* @param method 方法
* @param args 参数
* @return 方法调用结果
* @throws Throwable 异常
*/
//2.代理的逻辑方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//洗手
this.dinnerInterceptor.beforeDinner();
//吃饭
Object obj=null;
boolean flag=false;//表示是否发生异常
try{
obj=method.invoke(target,args);
}catch (Exception e){
flag=true;
}finally {
//不管有没有急事,吃完饭都要处理饭菜
this.dinnerInterceptor.after();
}
//处理急事
if(flag){
this.dinnerInterceptor.afterThrowing();
}
//没急事就收拾餐具
else{
this.dinnerInterceptor.afterDinner();
}
return obj;
}
修改AopMain
//测试动态代理执行类
public class AopMain {
public static void main(String[] args){
//新建目标对象
DinnerService target=new LunchServiceImpl();
//绑定目标对象
DinnerService proxy=(DinnerService) DinnerProxy.bind(target,new DinnerInterceptorImpl());
//执行代理对象的invoke方法
proxy.have();
}
}
运行截图
抛出异常表示发生急事
4.2.2 设计者的思维
1.暴露设计接口
2.给出约定的流程图
3.屏蔽实现动态代理的细节(DinnerProxy类),因为这些比较抽象
4.对于开发者只需要知道接口即可
改动一下就可
新建BreakfastServiceImpl类
@Service
public class BreakfastServiceImpl implements DinnerService {
@Override
public void have() {
System.out.println("吃早饭");
//throw new RuntimeException("发生了急事");//模拟发生急事
}
}
通过这样子就能方便代理对象切换目标对象实现逻辑方法
4.3 AOP
4.3.1 AOP概念
4.3.2 AOP开发
1.定义切面
通过它可以描述AOP其他的信息,用来描述流程的织入
织入流程图
2.确定连接点
确定是在什么地方需要AOP,即在Spring中是什么类的什么方法(确定需要拦截什么方法)
目录结构
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
定义切面类(MyAspect) 和连接点(BreakfastServiceImpl.have()方法)
//定义切面,对应拦截器的概念
@Aspect
@Component//让切面能被扫描
public class MyAspect {
/** execution 拦截正则式匹配方法
* * 任何返回类型的方法
* com.springboot.chapter4.service.impl.BreakfastServiceImpl 目标对象的全限定名称
* have 目标对象方法
* (..) 任意参数匹配
*/
//前置通知,正则式:定义什么时候用AOP
@Before("execution(* com.springboot.chapter4.service.impl.BreakfastServiceImpl.have(..))")
public void before(){
System.out.println("前置通知");
}
//后置通知
@After("execution(* com.springboot.chapter4.service.impl.BreakfastServiceImpl.have(..))")
public void After(){
System.out.println("后置通知");
}
//异常通知
@AfterThrowing("execution(* com.springboot.chapter4.service.impl.BreakfastServiceImpl.have(..))")
public void AfterThrowing(){
System.out.println("异常通知");
}
//返回通知
@AfterReturning("execution(* com.springboot.chapter4.service.impl.BreakfastServiceImpl.have(..))")
public void AfterReturning(){
System.out.println("返回通知");
}
}
我们也可以用定义切点@Pointcut注解简化连接点的编写
//定义切点,用这个就不用重复写execution了
@Pointcut("execution(* com.springboot.chapter4.service.impl.BreakfastServiceImpl.have(..))")
public void pointcut(){
}
//前置通知,正则式:定义什么时候用AOP
@Before("pointcut()")//args(desc,date),获取对象方法参数,只能用于非环绕通知
public void before(){
System.out.println("前置通知");
}
//后置通知
@After("pointcut()")
public void After(){
System.out.println("后置通知");
}
//异常通知
@AfterThrowing("pointcut()")
public void AfterThrowing(){
System.out.println("异常通知");
}
//返回通知
@AfterReturning("pointcut()")
public void AfterReturning(){
System.out.println("返回通知");
}
IOC容器配置类
@EnableAspectJAutoProxy//驱动Spring AOP的注解
@Configuration
@ComponentScan(value = "com.springboot.chapter4.*")
public class AppConfig {
}
测试类
//测试动态代理执行类
public class AopMain {
public static void main(String[] args){
AnnotationConfigApplicationContext context=null;
try {
//创建IOC容器,根据AppConfig配置类
context=new AnnotationConfigApplicationContext(AppConfig.class);
//DinnerService有两个实现类,不能仅仅通过类型获取Bean,这里是名字+类型获取Bean
DinnerService service=context.getBean("breakfastServiceImpl",DinnerService.class);
service.have();
}finally {
//关闭IOC容器
context.close();
}
}
}
运行截图
AOP可以把大量公共的代码(吃饭前奏、饭后收拾、急事处理)提取出来,然后单独实现,开发者就只要是实现原有方法(吃饭),减少开发者的代码量
(实现)减少开发者的代码
在事务AOP织入中
4.3.3 环绕通知
@Before在切入点开始处切入内容
@After在切入点结尾处切入内容
@AfterReturning在切入点return内容之后切入内容(可以用来对处理返回值做一些加工处理)
@Around在切入点前后切入内容,并自己控制何时执行切入点自身的内容
@AfterThrowing用来处理当切入内容部分抛出异常之后的处理逻辑
环绕通知是一个取代原有目标对象方法的通知,也提供了回调原有目标对像方法的能力(尽量不用它,对代码改动大)
环绕通知可以获取一个ProceedingJoinPoint参数,里面的proceed方法可以回调目标对象方法
非环绕通知注解标注的方法上可用参数只能为JoinPoint(被Spring封装过的对象),环绕通知为ProceedingJoinPoint(里面有proceed方法用于回调目标对象方法),继承关系如下图
通知上获取参数流程
修改服务接口和实现类
public interface DinnerService {
public void have(String desc, Date date);
}
@Service
public class BreakfastServiceImpl implements DinnerService {
@Override
public void have(String desc, Date date) {
System.out.println("吃早饭,吃的是【"+desc+"】,时间是【"+date+"】");
}
}
@Service
public class LunchServiceImpl implements DinnerService {
@Override
public void have(String desc, Date date) {
System.out.println("吃午饭,吃的是【"+desc+"】,时间是【"+date+"】");
//throw new RuntimeException("发生了急事");//模拟发生急事
}
}
修改Aspect
//定义切面,对应拦截器的概念
@Aspect
@Component
public class MyAspect {
//定义切点,用这个就不用重复写execution了
@Pointcut("execution(* com.springboot.chapter4.service.impl.BreakfastServiceImpl.have(..))")
public void pointcut(){
}
//前置通知
@Before("pointcut() && args(desc,date)")//args(desc,date),传递对象方法参数,只能用于非环绕通知
public void before(JoinPoint joinPoint, String desc, Date date){
Object []args=joinPoint.getArgs();//获取目标对象方法参数
System.out.println("前置通知"+"参数为desc:"+desc+" date:"+date);//打印的是对象地址
}
//后置通知
@After("pointcut()")
public void After(){
System.out.println("后置通知");
}
//异常通知
@AfterThrowing("pointcut()")
public void AfterThrowing(){
System.out.println("异常通知");
}
//返回通知
@AfterReturning("pointcut()")
public void AfterReturning(){
System.out.println("返回通知");
}
//环绕通知
@Around("pointcut()")
public void Around(ProceedingJoinPoint joinPoint)throws Throwable{
System.out.println("环绕通知前");
Object[] objects=joinPoint.getArgs();//环绕通知获取方法参数
joinPoint.proceed();//回调目标对象方法
System.out.println("环绕通知后,desc:"+objects[0]);
}
}
修改AopMain
public class AopMain {
public static void main(String[] args){
AnnotationConfigApplicationContext context=null;
try {
//创建IOC容器,根据AppConfig配置类
context=new AnnotationConfigApplicationContext(AppConfig.class);
//DinnerService有两个实现类,不能仅仅通过类型获取Bean,这里是名字+类型获取Bean
DinnerService service=context.getBean("breakfastServiceImpl",DinnerService.class);
service.have("包子",new Date());
}finally {
//关闭IOC容器
context.close();
}
}
DEBUG方式运行并查看参数
4.3.4 引入
增强bean接口的功能,比如吃饭接口have方法参数desc为空,我就增加一个校验功能判断不能为空
目录结构
增加检测接口和实现类
//检测接口
public interface BreakFastValidator {
public boolean validator(String desc);
}
import com.springboot.chapter4.validator.BreakFastValidator;
import org.springframework.util.StringUtils;
public class BreakFastValidatorImpl implements BreakFastValidator {
@Override
public boolean validator(String desc) {
return StringUtils.isEmpty(desc);//为空放回false
}
}
在MyAspect类添加下面代码
//声明检测接口
@DeclareParents(value = "com.springboot.chapter4.service.impl.BreakfastServiceImpl+",
defaultImpl = BreakFastValidatorImpl.class)
private BreakFastValidator breakFastValidator;
修改AOPMain方法
public static void main(String[] args){
AnnotationConfigApplicationContext context=null;
try {
//测试引入增强接口功能
//创建IOC容器
context=new AnnotationConfigApplicationContext(AppConfig.class);
//Spring Bean,context.getBean()就是得到代理对象
DinnerService dinnerService=context.getBean("breakfastServiceImpl",DinnerService.class);
//转换为BreakFastValidator接口,调用Spring Bean增强的validator方法
//代理对象的下挂的接口DinnerService和BreakFastValidator之间可以相互强制转换
BreakFastValidator breakFastValidator=(BreakFastValidator) dinnerService;
String desc="";//假设描述为空
if(breakFastValidator.validator(desc)){
System.out.println("还没有具体吃什么");
}else {
//不为空执行方法
dinnerService.have("包子",new Date());
}
}finally {
//关闭IOC容器
context.close();
}
为什么可以强制转换BreakFastValidator breakFastValidator=(BreakFastValidator) dinnerService;?
因为代理对象可以下挂多个接口,那么代理对象可以在接口之间相互转换,即DinnerService和BreakFastValidator之间可以相互强制转换
运行截图
4.3.5 多切面和责任链模式
如果有多个切面,那么他们各个通知顺序是如何的?
建立三个切面(代码一样),之前切面注解注释掉
@Aspect
@Component
public class MyAspect1 {
@Pointcut("execution(* com.springboot.chapter4.service.Impl.BreakFastServiceImpl.have(..))")
public void pointcut(){
}
@Before("pointcut()")
public void Before(){
System.out.println("MyAspect1的Before方法");
}
@After("")
public void After(){
System.out.println("MyAspect1的After方法");
}
@AfterThrowing("")
public void AfterThrowing(){
System.out.println("MyAspect1的AfterThrowing方法");
}
@AfterReturning("")
public void AfterReturning(){
System.out.println("MyAspect1的AfterReturning方法");
}
}
AOPMain方法
AnnotationConfigApplicationContext context=new AnnotationConfigApplicationContext(AppConfig.class);
try{
DinnerService dinnerService=context.getBean("breakFastServiceImpl",DinnerService.class);
String desc="酸菜鱼";
dinnerService.have(desc,new Date());
}finally {
context.close();
}
运行截图
两种方法定义切面顺序,数字小的先执行,数字多少无所谓;接口优先级高于注解,但是用注解更方便
1.在切面类添加注解@Order(num)
2.在切面类实现接口Ordered
public int getOrder() {
return 0;
}
4.3.6 责任链模式
MyBatis的插件原理就是代理的代理