介绍
Spring两大核心,IOC和AOP,其中AOP用到了动态代理,常用于日志记录,性能统计,安全控制,事物处理,异常处理等,主要将与业务无关的逻辑从代码中抽离出来复用,实现解耦。
动态代理是在静态代理的基础上产生的,博主之前介绍过静态代理模式。静态代理模式对弊端是被代理对象需要实现其公共接口以及接口的所有方法。试想若在一个系统中要给所有对象的所有方法都加上日志记录,让所有代理对象都去实现其接口是不妥的,不能体现其复用性,耦合度也较高。
动态代理通过反射技术,在调用被代理的对象的方法时,动态增加一些操作。
核心操作
动态代理通过Proxy类创建代理对象,需要被代理对象有接口的实现。
//CLassLoader loader:类的加载器
//Class<?> interfaces:得到全部的接口
//InvocationHandler h:得到InvocationHandler接口的子类的实例
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
我们通过Proxy的newProxyInstance(…)方法创建代理对象,其有三个参数,前两个参数是易懂的,InvocationHandler用来对被代理对象的原方法进行拦截,给被代理对象增加逻辑的,如日志记录。
//Object proxy:被代理的对象
//Method method:要调用的方法
//Object[] args:方法调用时所需要参数
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
动态代理通过InvocationHandler给被代理对象增加逻辑,其进一步通过InvocationHandler的invoke方法给代理对象增加逻辑,之后通过调用method.invoke(真实被代理对象,方法参数)来调用被代理对象的逻辑。
示例
一个常见的需求是给业务方法增加日志,这里通过动态代理,以简单的输出到控制器为例。
业务接口:
package test;
public interface UserManager { //业务接口
public void addUser();
}
接口实现:
package test;
public class UserManagerImpl implements UserManager{
@Override
public void addUser() {
System.out.println("A user added");
}
}
创建代理的工厂:
package test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyFactory {
public static Object createLogProxy(final Object arg){
Object obj = Proxy.newProxyInstance(arg.getClass().getClassLoader(), arg.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("start method->>" + method.getName()); //增加日志记录
method.invoke(arg, args);
System.out.println("end method->>" + method.getName()); //增肌日志记录
return proxy;
}
});
return obj;
}
}
客户端使用代理对象:
package test;
import org.junit.Test;
public class ProxyTest {
@Test
public void test(){
UserManager manager = (UserManager) ProxyFactory.createLogProxy(new UserManagerImpl());
manager.addUser();
}
}
输出结果:
start->>addUser
A user added
end->>addUser
注解
一种常见的场景就是动态代理和注解结合。注解在Spring中被应用的淋漓尽致,我们可以通过注解实现IOC,AOP,其大大提高了程序的开发效率,简化了开发流程。注解技术是面向框架级开发的,一般在往常的业务开发中较少使用注解来添加配置信息。在Spring中,AOP的实现可以用配置文件加动态代理实现,亦可以使用注解加动态代理实现,事实上,注解充当的就是一种配置的作用。
接下来将介绍Java中常见的几种元注解,以及如何自定义注解,如何获取注解中信息等。
JDK的元注解
所谓的元注解即给自定义注解使用的注解,一般表示你定义的注解用在什么地方,作用范围是什么。
JDK提供的几种元注解有@Retention、@Target、@Document、@Inherited。
另外必须指出的一点是:注解都是通过(属性名=值)的方式进行定义,若属性名为value,可以在定义时省略。如下的@Retention就省略了“value=”。
- @Retention Retention注解有如下几种,一般情况下,我们定义的注解都是RUNTIME,即给运行的程序使用。
@Retention(RetentionPolicy.SOURCE): 在.java文件中有效,给编译器使用 @Retention(RetentionPolicy.class):在class文件中有效,给类加载器使用 @Retention(RetentionPolicy.RUNTIME):给运行程序使用
- Target Target注解表示你所定义的注解的目标是什么,其有如下三种。
@Target(value={ElementType.METHOD,ElementType.TYPE,ElementType.FILED})
我们发现,其使用了一个数组进行定义,表示你定义的注解可以使用在方法、类/接口/enum、属性。
- Documented 使用了该注解定义的注解,注解信息会生成到文档。
- Inherited 应用了该注解的注解,会被自动继承到子类。如你希望你某个接口下的所有实现类都使用你定义的注解,那么无需在每个实现类中都去增加,你只需在其接口类中使用该注解即可轻易派生到其子类或实现类。
自定义注解
以上介绍了JDK的元注解,即给自定义注解使用的注解。如何自定义一个注解?如下所示:
@Retention(RetentionPolicy.RUNTIME) //表示该注解在程序运行时生效 @Target({ElementType.METHOD}) //表示该注解应用在方法上 public @interface PersonInfo{ String name(); int age(); int id() default 0; }
如何应用注解?所加的注解并不能直接生效,需要通过反射的方法解析注解。
接下来以Spring IOC中给属性添加,自动给属性赋值为例。
一种常见的场景是,DAO的配置,在DAO层通常有多个DAO的实现类,如MysqlDao,SqlserverDao等。给配置了注解的属性动态赋值,其需要通过解析该属性,并获取注解,之后拿到注解中所配置的类信息,最后生成实例将其写入到属性中。
1、定义注解:import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) public @interface DBInfo { String value(); }
2、在类的属性上应用注解
UserDao接口public interface UserDao { public void save(); //为了简单起见省略参数 public void get(); }
UserDaoImpl实现类
public class UserMysqlDao implements UserDao { @Override public void save() { System.out.println("A user saved to mysql..."); } @Override public void get() { System.out.println("Got a user from mysql..."); } }
public class UserSqlserverDao implements UserDao{ @Override public void save() { System.out.println("A user saved to sqlserver..."); } @Override public void get() { System.out.println("Got a user from sqlserver..."); } }
添加注解
public class UserService { @DBInfo("UserMysqlDao") private UserDao userDao; public void addUser(){ userDao.save(); } public void getUser(){ userDao.get(); } }
3、解析注解,赋予属性
import java.lang.reflect.Field; public class Main { public static void main(String[] args) throws Exception { UserService service = createUserService(); service.addUser(); } //在Spring框架,这个部分交给Spring容器管理,为了简单起见,这里使用一个静态方法 public static UserService createUserService() throws Exception{ UserService service = new UserService(); Class<?> clazz = service.getClass(); Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { if(field.isAnnotationPresent(DBInfo.class)){ //先判断是否含有该注解 DBInfo dbAnnotation = field.getAnnotation(DBInfo.class); //获取该注解 String dbClassStr = dbAnnotation.value(); //获取注解里的信息 Class<?> dbClass = Class.forName(dbClassStr); //生成目标实例对象 Object obj = dbClass.newInstance(); //通过反射创建示例对象 field.setAccessible(true); //设置属性可以被访问 field.set(service, obj); //对该属性赋值 } } return service; } }
在上述注解的解析流程中,无需关心DBInfo中的配置是什么,也无需关心属性名是什么,而只需关心把含有DBInfo配置的属性都动态赋值成注解中所配置的类的示例对象。
输出结果:
A user saved to mysql…动态代理结合注解
在Sprong AOP中,开发人员使用一个注解就能对其进行事物处理或者权限控制。其原理为通过使用动态代理和注解进行结合。
接下来以权限控制为例。
在上面的例子中,我们希望给Dao增加权限控制。对save方法判断当前用户是否具有该权限,权限的读取应该去数据库中检查,为了简单起见,这里直接定义了一个数组表示该用户具有的权限。增加自定义注解
修改UserDao接口
import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Inherited public @interface PrivilegeInfo { String value(); }
给需要检查权限的方法添加注解
public interface UserDao { @PrivilegeInfo("save") public void save(); //为了简单起见省略参数 public void get(); }
结合动态代理解析注解
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class ProxyFactory { public static Object createPrivilegeProxy(final Object arg){ Object obj = Proxy.newProxyInstance(arg.getClass().getClassLoader(), arg.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if(method.isAnnotationPresent(PrivilegeInfo.class)){ //权限校验 //原本应该去数据库中判断当前用户的权限,这里简单起见就定义一个数组 String privileges[] = {"save","get"}; //这些权限来自于数据库 for (String privilege : privileges) { if(privilege.equals(method.getAnnotation(PrivilegeInfo.class).value())){ method.invoke(arg, args); System.out.println("拥有权限"); } } }else{ //没有配置注解,直接放行 method.invoke(arg, args); } return proxy; } }); return obj; } }
在初始化对象时
import java.lang.reflect.Field; public class Main { public static void main(String[] args) throws Exception { UserService service = createUserService(); service.addUser(); } //在Spring框架,这个部分交给Spring容器管理,为了简单起见,这里使用一个静态方法 public static UserService createUserService() throws Exception{ UserService service = new UserService(); Class<?> clazz = service.getClass(); Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { if(field.isAnnotationPresent(DBInfo.class)){ DBInfo dbAnnotation = field.getAnnotation(DBInfo.class); String dbClassStr = dbAnnotation.value(); Class<?> dbClass = Class.forName(dbClassStr); Object obj = dbClass.newInstance(); field.setAccessible(true); //设置属性可以被访问 field.set(service, ProxyFactory.createPrivilegeProxy(obj)); //设置成代理对象 } } return service; } }
输出结果:
A user saved to mysql…
拥有权限总结
代理模式注重对原方法的拦截,增加新的逻辑,是权限校验还是日志记录还是事物处理,根据实际需求进行定义。在本节中,讨论了动态代理的优点,以及如何编写一个动态代理类来处理日志。又讨论了注解的使用方法,动态代理与注解技术相结合的例子。
参考文献
https://blog.csdn.net/qq_25956141/article/details/83692730
https://blog.csdn.net/hejingyuan6/article/details/36203505#