反射,在Java常用框架中屢見不鮮。它存在於java.lang.reflact包中,就我的認識,它可以拿到類的字段和方法,及構造方法,還可以生成對象實例等。對深入的機制我暫時還不了解,本篇文章着重在使用方面,並附上一個本人應用到項目中的案例。
- 基礎姿勢
拿到類,反射是以類為基礎的基礎,首先拿到項目中的類,既可以這樣拿
Class<?> clazz = Class.forName(類路徑);
也可以這樣拿
Class<?> clazz = 類名.getClass();
在一般意義的JavaBean中,存在構造函數、字段、一般函數三中不同元素,只要拿到了類,接着拿到它們就是水到渠成
Constructor constructors = clazz.getConstructor(null);//拿到構造函數 Field[] fields = clazz.getDeclaredFields();//拿到它定義的所有字段 Method[] methods = clazz.getDeclaredMethods();//拿到它定義的所有方法
注意,拿到無參的構造函數傳入的是null,拿到有參構造函數,則按照構造函數的參數位置傳入對應的類型class就行,比如
Constructor constructors = clazz.getConstructor(String.class,Integer.class,Double.class);//拿到有參構造函數
拿到他們有什么用?拿到構造函數還好可以新建對象,拿到字段呢?這時候就得配合自定義注解來使用了?
定義一個自定義標簽
import java.lang.annotation.*; @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) //@Documented public @interface AnnotationDemo { public String name(); public String value(); }
ElementType是作為標志存在的,而這個RetentionPolicy則是對功能上有影響的,它里面有三種策略。從源碼上看它存在CLASS,RUNTIME,SOURCE三種方式。這個Documented是生成java文檔時候是否帶上的意思。
package java.lang.annotation; /** * Annotation retention policy. The constants of this enumerated type * describe the various policies for retaining annotations. They are used * in conjunction with the {@link Retention} meta-annotation type to specify * how long annotations are to be retained. * * @author Joshua Bloch * @since 1.5 */ public enum RetentionPolicy { /** * Annotations are to be discarded by the compiler. */ SOURCE, /** * Annotations are to be recorded in the class file by the compiler * but need not be retained by the VM at run time. This is the default * behavior. */ CLASS, /** * Annotations are to be recorded in the class file by the compiler and * retained by the VM at run time, so they may be read reflectively. * * @see java.lang.reflect.AnnotatedElement */ RUNTIME }
官方解釋是,這個注解會留在編譯器,class文件,和VM中,我理解為作用范圍。一般使用RUNTIME。
接着造一個bean。
public class DemoBean { public String pubField; protected String protectField; String defaultField; @AnnotationDemo(name="test",value="123") private String priField; public DemoBean(){ this("pub","protect","default","pri"); } public DemoBean(String pubField,String protectField,String defaultField,String priField){ this.pubField = pubField; this.protectField = protectField; this.defaultField = defaultField; this.priField = priField; } public void function1(){ System.out.println("public function"); } protected void function2(){ System.out.println("protect function"); } void function3(){ System.out.println("default function"); } private void function4(){ System.out.println("private function"); } }
在拿到class之后,遍歷它的field尋找注解,當然了,對method也可以這樣。
Field[] fields = clazz.getDeclaredFields();//拿到它定義的所有字段 for(Field field:fields){ if(field.isAnnotationPresent(AnnotationDemo.class)){ System.out.println("有注解"); } AnnotationDemo annotationDemo = field.getAnnotation(AnnotationDemo.class); if(annotationDemo != null){ System.out.println("注解 name:"+annotationDemo.name()); System.out.println("注解 value:"+annotationDemo.value()); } System.out.println("屬性:"+field.getName()+" "+field.getModifiers()); }
這種方式是不是很眼熟啊?沒錯,Spring里面到處都是。
- 實際應用
在給字段和方法打上標簽之后,繁瑣的,重復性的行為都讓框架為你處理,這種開發方式節省了很多代碼,加強了閱讀性,是非常提升效率的。
反射有種方式是繞過編譯器對字段封裝性的限制的,也就是無論是public還是private的字段,在反射的程序中都是可以拿到並且改變它的值的。我們知道Spring框架對bean的注入,有好幾種方式。最讓人想的清楚的是構造方法注入和setter注入。而@autowired呢?即使不提供暴露接口一樣可以設置,這就是利用了反射的方式。
經過查閱資料,閱讀源碼,在Spring-bean中尋找到這兩個類,因為設計圖太過復雜,本人只能在細小之處分析了。
/** * Populate the bean instance in the given BeanWrapper with the property values * from the bean definition. * @param beanName the name of the bean * @param mbd the bean definition for the bean * @param bw BeanWrapper with bean instance */ protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) { PropertyValues pvs = mbd.getPropertyValues(); if (bw == null) { if (!pvs.isEmpty()) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Cannot apply property values to null instance"); } else { // Skip property population phase for null instance. return; } } // Give any InstantiationAwareBeanPostProcessors the opportunity to modify the // state of the bean before properties are set. This can be used, for example, // to support styles of field injection. boolean continueWithPropertyPopulation = true; if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof InstantiationAwareBeanPostProcessor) { InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) { continueWithPropertyPopulation = false; break; } } } } if (!continueWithPropertyPopulation) { return; } if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME || mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) { MutablePropertyValues newPvs = new MutablePropertyValues(pvs); // Add property values based on autowire by name if applicable. if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) { autowireByName(beanName, mbd, bw, newPvs); } // Add property values based on autowire by type if applicable. if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) { autowireByType(beanName, mbd, bw, newPvs); } pvs = newPvs; } boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors(); boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE); if (hasInstAwareBpps || needsDepCheck) { PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching); if (hasInstAwareBpps) { for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof InstantiationAwareBeanPostProcessor) { InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName); if (pvs == null) { return; } } } } if (needsDepCheck) { checkDependencies(beanName, mbd, filteredPds, pvs); } } applyPropertyValues(beanName, mbd, bw, pvs); }
經過一系列前置驗證(看得迷迷糊糊)然后進行bean注入,調用的是postProcessPropertyValues方法,點進去是個接口(多態性是挺坑的)。找了一下,最后找到AutowiredAnnotationBeanPostProcessor類,里面有個內部類AutowiredMethodElement,其中有個Inject方法是實施注入的。
public static void makeAccessible(Field field) { if((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers()) || Modifier.isFinal(field.getModifiers())) && !field.isAccessible()) { field.setAccessible(true); } }
@CallerSensitive public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, obj, modifiers); } } MethodAccessor ma = methodAccessor; // read volatile if (ma == null) { ma = acquireMethodAccessor(); } return ma.invoke(obj, args); }
前一個是打開權限,后一個粗略看了下,就是注入了。
大概的思路就是使用反射打開權限,然后從對象池中拿取對象並設置到該字段中。如果對象池沒有,大概就是放個空進去了,最后調用的時候就是空指針了。
- 實踐案例
未能吸取Spring優秀框架的思想,但是自己在項目中應用了一下這種技術。小型項目對建表的要求就是,新建一個bean自動生成一個表。是不是很像某ORM?是的,僅僅使用幾百行代碼就可以實現這個功能了,采用的就是以反射、Annotation為基礎的技術。
package Common; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.HashSet; import java.util.Properties; import java.util.Set; import org.apache.log4j.Logger; import Common.Annotation.ATable; import Common.Annotation.AutoIncrement; import Common.Annotation.Column; import Common.Annotation.PrimaryKey; /** * 初始化數據庫 按照Model包下的類及字段創建 * @author ctk * */ public class InitDataBases { private static Logger logger = Logger.getLogger(InitDataBases.class); private Connection conn = null; private Set<String> tables; //單例 private static InitDataBases instance = new InitDataBases(); private InitDataBases(){ conn = getConnection(); tables = new HashSet<>(); searchTables(); } /** * 讀取數據庫資源文件 * 獲得數據庫鏈接 * @return */ private Connection getConnection(){ logger.debug("建立數據庫連接"); String driver = ""; String url = ""; String username = ""; String password = ""; File f = new File(this.getClass().getClassLoader().getResource("/").getPath()+"jdbc.properties"); Properties pro = new Properties(); InputStream in = null; try { in = new FileInputStream(f); pro.load(in); driver = pro.getProperty("jdbc.driverClass"); url = pro.getProperty("jdbc.url"); username = pro.getProperty("jdbc.username"); password = pro.getProperty("jdbc.password"); } catch (FileNotFoundException e) { logger.error("資源文件未找到,請命名為jdbc.properties,並置於src下"); System.err.println("資源文件未找到,請命名為jdbc.properties,並置於src下"); } catch (IOException e) { logger.error("資源文件讀寫異常"); System.err.println("資源文件讀寫異常"); } Connection conn = null; try { Class.forName(driver); conn = DriverManager.getConnection(url,username,password); } catch (ClassNotFoundException e) { logger.error("加載驅動不成功,請檢查是否添加了jdbc的必要包"); System.err.println("加載驅動不成功,請檢查是否添加了jdbc的必要包"); } catch (SQLException e) { logger.error("數據庫連接錯誤,檢查賬號密碼和數據庫地址"); System.err.println("數據庫連接錯誤,檢查賬號密碼和數據庫地址"); } return conn; } /** * 自動建表 * @param clazz */ public void checkAndCreate(Class<?> clazz){ String tableName = getTableName(clazz); if(tableExist(tableName)) { logger.debug(tableName+"表已存在"); return; } Field[] fields = clazz.getDeclaredFields(); StringBuilder sb = new StringBuilder("create table "); sb.append(tableName); sb.append(" ("); for(int i=0;i<fields.length;i++){ PrimaryKey pk = fields[i].getAnnotation(PrimaryKey.class); sb.append(getColumnName(fields[i])); sb.append(" "); Class<?> type = fields[i].getType(); if(type == String.class) sb.append("VARCHAR(255)"); else if(type == int.class) sb.append("INT(50)"); else if(type == long.class) sb.append("BIGINT(20)"); else if(type == double.class || type == float.class) sb.append("DOUBLE"); //如果是主鍵字段 if(pk != null){ sb.append(" primary key"); AutoIncrement ai = fields[i].getAnnotation(AutoIncrement.class); //判斷是否自增 if(ai != null){ sb.append(" AUTO_INCREMENT"); } } if(i != (fields.length-1)) sb.append(","); } sb.append(")DEFAULT CHARSET=utf8"); logger.debug("sql:"+sb.toString()); try { PreparedStatement pst = conn.prepareStatement(sb.toString()); pst.execute(); } catch (SQLException e) { logger.error("建表錯誤:"+e.getMessage()); } } /** * 獲得表名字 * @param clazz * @return */ public String getTableName(Class<?> clazz){ //獲得表別名 ATable table = clazz.getAnnotation(ATable.class); if(table != null && "".equals(table.name())) return table.name(); else { return clazz.getSimpleName(); } } /** * 獲取列名稱 * @param field * @return */ public String getColumnName(Field field){ Column column = field.getAnnotation(Column.class); if(column != null){ return column.value(); }else{ return field.getName(); } } /** * 查詢表是否存在 * @return */ private void searchTables(){ String sql = "show tables"; try { PreparedStatement pst = conn.prepareStatement(sql); logger.debug("sql執行:"+sql); ResultSet rset = pst.executeQuery(); while(rset.next()){ String tname = rset.getString(1); tables.add(tname); } } catch (SQLException e) { logger.error("sql錯誤:"+e.getMessage()); } } /** * 判斷是否存在某數據 * @param sql * @return */ public boolean dataExist(String sql){ try { PreparedStatement pst = conn.prepareStatement(sql); logger.debug("sql執行:"+sql); ResultSet rset = pst.executeQuery(); long id = 0; while(rset.next()){ id = rset.getLong("id"); } if(id == 0) return false; else return true; } catch (SQLException e) { logger.error("sql錯誤:"+e.getMessage()); return false; } } /** * 插入數據sql * @param sql */ public void insertSql(String sql){ try { PreparedStatement pst = conn.prepareStatement(sql); logger.debug("sql執行:"+sql); pst.execute(); } catch (SQLException e) { logger.error("sql錯誤:"+e.getMessage()); } } //獲得單例 public static InitDataBases getInstance(){ return instance; } public boolean tableExist(String table) { return tables.contains(table); } //關閉鏈接 public void closeConn(){ logger.debug("關閉數據庫連接..."); try { conn.close(); } catch (SQLException e) { logger.error("關閉數據庫連接失敗:"+e.getMessage()); } } }
然后在啟動的Listener中加入。
CommonInfo.FilePackage = this.getClass().getClassLoader().getResource("/").getPath()+"/WEB-INF/fildDownload"; File pkg = new File(CommonInfo.FilePackage); if(!pkg.exists()) pkg.mkdirs(); File f = new File(this.getClass().getClassLoader().getResource("/").getPath()+"/Model"); File[] fs = f.listFiles(); List<String> tables = new ArrayList<>(); for(File fl:fs){ String fname = fl.getName(); fname = fname.substring(0,fname.length()-6); tables.add(fname); } InitDataBases initDB = InitDataBases.getInstance(); try { for (String table : tables) { Class<?> clazz = Class.forName("Model." + table); initDB.checkAndCreate(clazz); } } catch (ClassNotFoundException e) { logger.error("找不到bean:"+e.getMessage());;
大概的思路就是,檢查bean文件夾下是否存在bean並且bean中是否有Annotation修飾,並拼湊建表語句,最后新建表,別忘了關閉數據庫連接。