SSM-Spring(三)-代理、AOP
代理简介
- 业务层(Service)的主要内容
- 业务代码:业务运算、dao操作等(必要)
- 最终的结果需要通过几个数据库查询然后计算得出
- 附加代码:事务、日志、性能监控、异常处理等(可选)
- 查询几个数据库必须有条件,同时成功、失败等
- 计算结果完成还需要将结果记录日志
- 对整段业务代码进行trycatch
- 业务代码:业务运算、dao操作等(必要)
- 存在问题:在业务层加入附加代码会显得很臃肿、累赘,但很多时候又不得不加
- 代理
- 在不修改目标类的目标方法代码的前提下,为目标方法增加额外功能
- 代理的必要条件: 代理类中必须也有同样的目标方法
- 代理类实现跟目标类同样的接口
- 若目标类没有实现(implements)接口,代理类继承目标类
- 代理的实现方案
- 静态代理(Static Proxy)
- 开发人员手动编写代理类(创建对应的*.java文件)
- 基本上,一个目标类就要编写一个代理类
- 动态代理(Dynamic Proxy)::程序运行过程动态生成代理类的字节码
- 静态代理(Static Proxy)
- 使用代理的好处:业务层(service)只需要关注返回给客户端数据的封装即可,其他有代理类实现。
静态代理
- 业务层service
-
UserService
//接口 public interface UserService { boolean login(String username, String password); } //实现类 public class UserServiceImpl implements UserService{ //业务方法 @Override public boolean login(String username, String password) { System.out.println("09 - UserServiceImpl - login - " + username + "_" + password); return false; } }
-
SkillService
//只有目标实现类,没有接口 public class SkillService { public boolean save(Object skill) { System.out.println("SkillServiceImpl - save"); return false; } }
-
- 手动实现对应业务层类的代理类
-
UserServiceProxy
//代理类实现跟目标类同样的接口 public class UserServiceProxy implements UserService { private UserService target; public void setTarget(UserService target) { this.target = target; } @Override public boolean login(String username, String password) { System.out.println("日志------------------1"); boolean result = target.login(username, password); System.out.println("日志------------------2"); return result; } }
-
SkillServiceProxy
//若目标类没有实现接口,代理类继承目标类 public class SkillServiceProxy extends SkillService { private SkillService target; public void setTarget(SkillService target) { this.target = target; } @Override public boolean save(Object skill) { System.out.println("SkillServiceProxy - 1"); boolean result = target.save(skill); System.out.println("SkillServiceProxy - 2"); return result; } }
-
-
applicationContext.xml中注册类
<!--代理对象1--> <bean id="userService" class="com.zh.proxy.UserServiceProxy"> <!--设置目标类(委托方) --> <property name="target"> <bean class="com.zh.service.impl.UserServiceImpl"/> </property> </bean> <!--代理对象2--> <bean id="skillService" class="com.zh.proxy.SkillServiceProxy"> <!--设置目标类(委托方) --> <property name="target"> <bean class="com.zh.service.SkillService"/> </property> </bean>
-
UserTest
@Test public void test() { // 创建容器 ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); //此时拿到的是代理对象UserServiceProxy,不是UserServiceImpl对象 UserService userService = ctx.getBean("userService", UserService.class); service.login("123", "456"); SkillService skillService = ctx.getBean("skillService", SkillService.class); service.save(null); // 关闭容器 ctx.close(); }
动态代理(Dynamic Proxy)
- 动态代理的常见实现方案有2种
- JDK自带的API
- 本质:代理类实现跟目标类一样的接口
- 开源项目GGLib(Code Generation Library)
- 本质:代理类继承目标类
- Spring已经集成了GGLib
- JDK自带的API
动态代理-JDK
- 本质就是通过JDK自带的API动态的实现一个对象,这个对象与目标类有相同的实现方法(通过与目标类实现相同的接口)
-
目标类,service业务层
//接口 public interface UserService { boolean login(String username, String password); } //实现 public class UserServiceImpl implements UserService { @Override public boolean login(String username, String password) { System.out.println("UserServiceImpl - login"); return false; } }
-
applicationContext.xml中注册类
<bean id="userService" class="com.zh.service.impl.UserServiceImpl"/>
-
UserTest
@Test public void test() { // 创建容器 ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); // 目标对象 UserServiceImpl target = ctx.getBean("userService", UserServiceImpl.class); // 代理对象 //JDK的API,通过传递类加载器、需要实现的接口、附加block,动态生成一个对象 UserService userService = (UserService) Proxy.newProxyInstance( getClass().getClassLoader(), // 类加载器 target.getClass().getInterfaces(), // 代理类需要实现的接口(目标类的接口) (Object proxy, Method method, Object[] args) -> { // 附加代码(代理类的具体实现) // proxy:代理对象 // method:目标方法 // args:目标方法的参数 System.out.println("proxy - 1"); // 调用目标对象的目标方法(核心业务代码) Object result = method.invoke(target, args); System.out.println("proxy - 2"); return result; }); //通过代理对象调用目标方法,会调用它的block代码 userService.login("123", "456"); // 关闭容器 ctx.close(); }
- 分析:
- 上面的方法实现,如果有n个类,那么动态的实现需要写n段相同的代码
- 如果n个类要同时实现,比如service层的类都要实现呢?
- 此时可以用上节讲的生命周期方法拦截所有的对象,一次性处理
- 经过排查可以使用postProcessAfterInitialization方法中进行拦截
统一封装设置动态代理
-
创建LogProcessor类
//com.zh.processor.LogProcessor /** * 会拦截每一个bean的生命周期 * 监听初始化操作,每个bean对象只会执行一次 */ public class LogProcessor implements BeanPostProcessor { //初始化完毕,然后生成代理 @Override public Object postProcessAfterInitialization(Object target, String beanName) throws BeansException { return Proxy.newProxyInstance( getClass().getClassLoader(), // 类加载器 target.getClass().getInterfaces(), // 代理类需要实现的接口(目标类的接口) new LogInvocationHandler(target)); // 附加代码; } //封装一个类 private static class LogInvocationHandler implements InvocationHandler { private final Object target; public LogInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("proxy - 1"); // 调用目标对象的目标方法(核心业务代码) Object result = method.invoke(target, args); System.out.println("proxy - 2"); return result; } } }
-
applicationContext.xml中注册
<bean class="com.zh.processor.LogProcessor"/>
-
UserTest
@Test public void test3() { // 创建容器 ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); //此时获取的就是代理对象userService UserService userService = ctx.getBean("userService", UserService.class); userService.login(null, null); }
动态代理-GGLib
- 本质是使用Spring内部集成GGLib的API来动态的实现一个对象,这个对象与目标类有相同的实现方法(通过继承目标类来实现)
-
目标类,service业务层
//只有实现,没有接口 public class SkillService { public boolean save(Object skill) { System.out.println("SkillService - save"); return false; } }
-
统一封装拦截所有对象创建,动态实现代理
public class LogProcessor2 implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object target, String beanName) throws BeansException { //过滤哪些对象需要设置代理 //if (beanName.equals("person")) return target; if (!beanName.endsWith("Service")) return target; Enhancer enhancer = new Enhancer(); // 可以省略,内部自动获取类加载器 // enhancer.setClassLoader(getClass().getClassLoader()); //代理类继承目标类 enhancer.setSuperclass(target.getClass()); //附加代码 enhancer.setCallback(new LogMethodInterceptor(target)); //返回代理 return enhancer.create(); } //MethodInterceptor方法拦截器接口,callback的参数 private static class LogMethodInterceptor implements MethodInterceptor { private final Object target; public LogMethodInterceptor(Object target) { this.target = target; } @Override public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { String name = method.getName(); // method.getReturnType(); // method.getParameterTypes(); System.out.println("1-------------------"); Object result = method.invoke(target, args); System.out.println("2-------------------"); return result; } } }
-
applicationContext.xml中注册
<bean id="skillService" class="com.zh.service.SkillService"/> <bean class="com.zh.processor.LogProcessor2"/>
-
UserTest
// 创建容器 ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); SkillService skillService = ctx.getBean("skillService", SkillService.class); skillService.save(null); UserService userService = ctx.getBean("userService", UserService.class); userService.login(null, null);
缺点分析
- 某些类不需要代理,需要进行拦截设置
- 不同的类有不同的附加代码,需要不同设置
- 所有的拦截都放在LogProcessor,代码极度冗余
- 解决办法—AOP:可以实现简单控制,哪些类需要实现代理,哪些方法需要实现附加方法
AOP(Aspect Oriented Programming)
- AOP又叫面向切面编程
- Spring使用AOP技术封装了动态代理的功能,使用起来非常简单
-
它依赖于AspectJ库
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.9.6</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.6</version> </dependency>
- 附加代码可以写在下面两个类中
- MethodBeforeAdvice
- MethodInterceptor
MethodBeforeAdvice
-
新建一个类实现MethodBeforeAdvice接口,用来编写额外功能(会在目标方法执行之前执行)
//com.zh.aop.LogAdvice public class LogAdvice implements MethodBeforeAdvice { @Override //只能在目标方法的前面调用 public void before(Method method, Object[] args, Object target) throws Throwable { System.out.println("LogAdvice - before ------------------"); } }
-
applicationContext.xml中注册
<bean id="userService" class="com.zh.service.impl.UserServiceImpl"/> <bean id="personService" class="com.zh.service.impl.PersonServiceImpl"/> <bean id="skillService" class="com.zh.service.SkillService"/> <!-- 附加代码--> <bean id="logAdvice" class="com.zh.aop.LogAdvice"/> <!-- 切面 --> <aop:config> <!-- 切入点:给哪些类的哪些方法增加附加代码? --> <!-- execution(* *(..))代表所有类的所有方法都会被切入 --> <aop:pointcut id="pc" expression="execution(* *(..))"/> <!-- 附加代码设置给指定的切入点 --> <aop:advisor advice-ref="logAdvice" pointcut-ref="pc"/> </aop:config>
-
UserTest
public void test() { // 创建容器 ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); SkillService skillService = ctx.getBean("skillService", SkillService.class); skillService.save(null); UserService userService = ctx.getBean("userService", UserService.class); userService.login(null, null); userService.register(null); PersonService personService = ctx.getBean("personService", PersonService.class); personService.run(); }
-
打印:
LogAdvice - before ------------------ SkillService - save LogAdvice - before ------------------ UserServiceImpl - login LogAdvice - before ------------------ UserServiceImpl - register LogAdvice - before ------------------ PersonServiceImpl - run
MethodInterceptor
-
新建一个类实现
org.aopalliance.intercept.MethodInterceptor
接口,用来编写额外功能(可以灵活设置,在目标代码前面还是后面插入)//com.zh.aop.LogInterceptor public class LogInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { System.out.println("1 ----------------"); // 调用目标对象的目标方法 Object result = invocation.proceed(); System.out.println("2 ----------------"); return result; } }
-
applicationContext.xml中附加代码修改为
<bean id="logInterceptor" class="com.zh.aop.LogInterceptor"/> <!-- 切面 --> <aop:config> <aop:pointcut id="pc" expression="execution(* *(..))"/> <aop:advisor advice-ref="logInterceptor" pointcut-ref="pc"/> </aop:config>
-
其余不变,打印如下
1 ---------------- SkillService - save 2 ---------------- 1 ---------------- UserServiceImpl - login 2 ---------------- 1 ---------------- UserServiceImpl - register 2 ---------------- 1 ---------------- PersonServiceImpl - run 2 ----------------
动态代理的底层实现
- Spring动态代理的底层实现
- 如果目标类有实现接口,使用JDK实现
- 如果目标类没有实现接口,使用CGLib实现
- 在BeanPostProcessor的postProcessAfterInitialization方法中创建代理对象
- 参考AbstractAutowireCapableBeanFactory的applyBeanPostProcessorsBeforeInitialization方法
- 可以通过proxy-target-class属性修改底层实现方案
- true:强制使用CGLib
- false:按照默认做法
<aop:config proxy-target-class="true"> </aop:config>
- 注意:使用JDK肯定比使用CGLib效率高
切入点表达式
-
在上面的示例中,切入点写的是如下
<aop:pointcut id="pc" expression="execution(* *(..))"/>
-
execution(* *(..))
代表所有类的所有方法都会被切入第一个*: 权限修饰符 返回值 第二个*: 包名、类名、方法名 (..): 参数 举例: expression=execution(public String com.zh.service.UserService.login(String,String)
- 那么我们如何表达某个类的某个方法需要切入呢?—-切入点表达式;参考资料:点击
-
切入点表达式格式:
expression="切入点指示符(表达式内容)"
-
常见的切入点指示符(pointcut designators)有
execution: 通用表达式 args: 专门用来指定参数 within: 专门用来指定包 @annotation: 专门用来指定有某个注解的方法
- 还可以使用
&&(and)、||(or)、!
组合切入点表达式 -
常见的切入点表达式
任意公共方法:execution(public * *(..)) 名字以set开头的任意方法: execution(* set*(..)) UserService接口定义的任意方法:execution(* com.zh.service.UserService.*(..)) service包中定义的任意方法:execution(* com.mj service.*.*(..)) service包中定义的任意方法(包括子包) :execution(* com.zh.service..*.*(..)) 包含2个String参数的任意方法:execution(* *(String,String)) 只有1个Serializable参数的任意方法:args(java.io.Serializable) 等价于: execution(* *(java.io.Serializable)) service包中的任意方法:within(com.zh.service.*) service包中的任意方法(包括子包) :within(com.zh.service..*) 带有自定义注解 (Hehe) 的方法:@annotation(com.zh.aop.Hehe)
- 举例:自对应一个注解,然后可以使用@annotation,专门筛选
-
自定义注解Log: 右击文件夹->new->Java Class ->选择Annotation,输入类名即可
//com.zh.annotaion.Log /* 限制注解范围:TYPE只能注解在类前面。METHOD:只能注解方法前面,FIELD:只能注解成员变量 {ElementType.METHOD,ElementType.TYPE}:两者皆可 */ @Target({ElementType.METHOD,ElementType.TYPE}) //注解什么时间有效:运行时RUNTIME、编译时SOURCE等 @Retention(RetentionPolicy.RUNTIME) public @interface Log {}
-
其他类使用
public class SkillService { private int age; @Log public boolean save(Object skill) { System.out.println("SkillService - save"); return false; } }
-
切入点表达式
<!--所有带@Log注解方法的所有类,都切入--> <aop:pointcut id="pc" expression="@annotation(com.zh.annotaion.Log)"/>
-
AOP细节问题
- 默认情况下,目标方法相互调用时,总共只会被切入1次。解决方法:拿到代理对象去调用目标方法
-
举例
//xml切入 <bean id="logInterceptor" class="com.zh.aop.LogInterceptor"/> <aop:config > <aop:pointcut id="pc" expression="execution(* *(..))"/> <aop:advisor advice-ref="logInterceptor" pointcut-ref="pc"/> </aop:config> //目标方法 public class UserServiceImpl implements UserService { @Override public boolean login(String username, String password) { System.out.println("UserServiceImpl - login"); register("123"); return false; } @Override @Log public boolean register(String username) { System.out.println("UserServiceImpl - register"); return false; } } //测试 ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); UserService userService = ctx.getBean("userService", UserService.class); userService.login(null, null);
-
login内部调用register,正常情况下打印结果应该是
1 ---------------- UserServiceImpl - login 1 ---------------- UserServiceImpl - register 2 ---------------- 2 ----------------
-
实际结果
1 ---------------- UserServiceImpl - login UserServiceImpl - register 2 ----------------
-
从实际结果来看,register并没有被切入,原因如下:
- ctx.getBean()返回的serService是动态生成的UserServiceImpl代理对象
- userService.login()调用的是代理对象LogInterceptor的invoke方法
- invoke方法内部先调用1,然后目标对象调用目标方法login:
Object result = invocation.proceed();
- 因为是目标对象调用login,所以login内部的register也是目标对象调用,而不是代理对象去调用register
- 综上原因才会得到上面打印的结果
-
-
如果需要register也能被切入,那么login方法内部应该也用代理对象去调用register,那么如何拿到代理对象呢?—使用生命周期方法
-
UserServiceImpl实现ApplicationContextAware、BeanNameAware接口,并实现代理方法
//设置类的成员变量 private ApplicationContext ctx; private String beanName; //目的是为了拿到容器,拿到代理对象 @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { ctx = applicationContext; } //为了拿到beanname @Override public void setBeanName(String name) { beanName = name; }
-
login内部用代理对象去调用register
@Override public boolean login(String username, String password) { System.out.println("UserServiceImpl - login"); //register("123"); //拿到代理对象去调用register ctx.getBean(beanName, UserService.class).register(username, password); return false; }
-
打印结果
1 ---------------- UserServiceImpl - login 1 ---------------- UserServiceImpl - register 2 ---------------- 2 ----------------
-
-
- 配置多个pointcut、advisor
-
当指定切入某几个类时,可以这么写
//写法1:写在一起 <aop:config> <aop:pointcut id="pc1" expression="within(com.zh.service.impl.UserServiceImpl)"/> <aop:pointcut id="pc2" expression="within(com.zh.service.SkillService)"/> <aop:advisor advice-ref="logInterceptor" pointcut-ref="pc1"/> <aop:advisor advice-ref="logInterceptor" pointcut-ref="pc2"/> <aop:advisor advice-ref="logAdvice" pointcut-ref="pc1"/> </aop:config> //写法2:每个类单独写 <aop:config> <aop:pointcut id="pc1" expression="within(com.mj.service.impl.UserServiceImpl)"/> <aop:advisor advice-ref="logInterceptor" pointcut-ref="pc1"/> <aop:advisor advice-ref="logAdvice" pointcut-ref="pc1"/> </aop:config> <aop:config> <aop:pointcut id="pc2" expression="within(com.mj.service.SkillService)"/> <aop:advisor advice-ref="logInterceptor" pointcut-ref="pc2"/> </aop:config>
-
可以使用
and、or、!
来实现几个条件同时满足<aop:config> <!-- 在com.zh.service.impl.UserServiceImpl类中切入,而且必须有2个参数的目标方法,才会被切入 --> <aop:pointcut id="pc" expression="within(com.zh.service.impl.UserServiceImpl) and args(String, String)"/> <aop:advisor advice-ref="logInterceptor" pointcut-ref="pc"/> </aop:config>
总结
-
- AOP表面上*实现了在不修改原来类的基础上改变原来类的某些方法,本质是新生成了一个代理类(继承、接口实现)提供给使用者为原来的方法增加一些附加代码。
- AOP仅仅是给原来类的方法新增一些附加代码,不会给原来类新增方法、属性。
- 注意: AOP以及上面的代理方式仅仅适用于spring的IOC容器创建bean对象的方式,不适用于手动通过new或者其他方式创建目标对象的方式。因为本质都是基于spring容器bean的生命周期方法来实现的。