????? 本文简介Java自定义注解的使用,并且结合在使用POI导出excel表格中的一个应用来加深对annotation的理解。预备知识:Java基础、反射机制、略微了解POI或JXL等读写EXCEL的工具。
????
Annontation(注解)是Java5开始引入的新特征。它用来将一些元数据/元信息(metadata)与程序元素(类、方法、成员变量等)进行关联,为程序的元素(类、方法、成员变量)加上更直观更明了的说明,并且供指定的工具或框架使用,起到说明、配置的功能。常用的注解如@Override,@Controller、@Repository等各种框架的注解等,Annontation像一种修饰符一样,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。
Annotation相关API包含在 java.lang.annotation 包中,使用
@interface关键字来声明一个自己的注解类型,并配合一系列的
元注解(@Retention @Target @Document @Inherited)来说明你的注解的信息
? 1、生成文档。这是java 最早提供的注解。常用的有@param @return 等
?
??2、实现替代配置文件功能。通常用于Java Config的配置方式中,用来替代XML(最常用功能)
????3、在编译时进行格式检查。如@override 放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出。
?
注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。而我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象$Proxy1。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从member
Values这个Map中索引出对应的值。而memberValues的来源是Java常量池
。
?
自定义注解类编写的一些规则:
? 1. Annotation型定义为@interface, 所有的Annotation会自动继承java.lang.Annotation这一接口,并且不能再去继承别的类或是接口.
? 2. 参数成员只能用public或默认(default)这两个访问权修饰
? 3. 参数成员只能用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和String、Enum、Class、annotation(
可组合别的注解,如@SpringBootApplication)等数据类型,以及这一些类型的数组.
? 4. 要获取类方法和字段的注解信息,必须通过Java的反射技术来获取 Annotation对象,因为你除此之外没有别的获取注解对象的方法
1)isAnnotationPresent
2)getAnnotation
——————————————————————————————————————————
下面通过一个简单的
例子来演示annotation的基本使用。该例子有以下三个组件:
1.组件1--水果名注解@FruitName。只有一个value属性,用于接收外界传入的水果名
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 水果名称注解
*/
@Target({ElementType.FIELD})//用于属性上注解
@Retention(RetentionPolicy.RUNTIME)//运行期也能起作用
@Documented//允许加入到javadoc中
public @interface FruitName {
String value() default "";
}
2. 组件2--一个实体类Apple,使用@FruitName注解来表示当前水果的名字
public
class Apple {
@FruitName(value="China.Apple")//相当于为fruitname的value属性赋值
private String appleName;
public String getAppleName() {
return appleName;
}
public void setAppleName(String appleName) {
this.appleName = appleName;
}
}
3.组件3--一个注解使用类,演示怎么利用反射来使用注解
import java.lang.reflect.Field;
public class FruitNameUtil {
public static void main(String[] args) {
Apple apple = new Apple();
Field[] fields = apple.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(FruitName.class)) {
//获取FruitName注解
FruitName fruitName = (FruitName) field.getAnnotation(FruitName.class);
String strFruitName = fruitName.value();
System.out.println("当前水果为:"+strFruitName);
}
}
}
}
大家可以看到,注解接口是一种特殊的接口,在业务类上使用特定注解标注,相当于创建了注解接口的一个实例,并且可以选择为该实例的某些属性赋值,比如FruitName注解的value属性。在具体使用注解时,通常要使用反射提取类、属性或方法上的注解(往往还会访问属性值),用于一些
全局性的判断,比如spring mvc的@Controller注解,spring框架在反射时就知道该类是一个web控制器类,如果读到@requestmapping注解,就知道这个方法上标注了请求的URI,并且通过其value属性值提取对应的URI。这里的核心API是isAnnotationPresent(注解接口对应的class实例)和getAnnotation(注解接口对应的class实例)两个方法,前者用于判断指定注解是否存在,后者用于返回注解实例,用于进一步操作。
——————————————————————————————————————————
POI导出Excel基本操作
Apache POI是Apache软件基金会的一个开源框架,提供API给Java程序对Microsoft
Office格式文件读和写的功能
。下面的例子用于展示POI读写EXCEL的基本操作,4个Junit测试方法分别用于向EXCEL写常量数据、写实体对象数据、增加标题以及从EXCEL中读取数据。
public class PoiDemoApplicationTests2 {
/*@Test
public void contextLoads() {
}*/
// 简单写常量数据
/**
* HSSFWorkbook,针对是 EXCEL2003
版本,扩展名为 .xls
* XSSFWorkbook,针对是Excel2007版本,扩展名为 .xlsx
* @throws Exception
*/
@Test
public void insertExcel1() throws Exception {
// 创建工作簿对象(Excel文件)
HSSFWorkbook workbook = new HSSFWorkbook();
// (new FileInputStream(new File("E:/temp/t1.xls")));
// 创建表格中一个sheet对象
HSSFSheet sheet = workbook.createSheet("mysheet1");
// int i = workbook.getSheetIndex("xt"); // sheet表名
// sheet = workbook.getSheetAt(i);
// 创建行对象,映射到表格中某一行
HSSFRow row = sheet.getRow(0);
if (row == null) {
row = sheet.createRow(0); // 该行无数据,创建行对象
}
// 创建一个单元格对象,参数为该单元格列数(从0开始)
Cell cell = row.createCell(0); // 创建指定单元格对象。如本身有数据会替换掉
cell.setCellValue("test data"); // 设置内容
FileOutputStream fo = new FileOutputStream(new File("E:/temp/t1.xls")); // 输出到文件
workbook.write(fo);
fo.close();
}
// 写入对象数据
@Test
public void insertExcel2() throws Exception {
// 创建工作簿对象(Excel文件)
HSSFWorkbook workbook = new HSSFWorkbook();
HSSFSheet sheet = workbook.createSheet("mysheet2");
Memeber m1 = new Memeber();
Memeber m2 = new Memeber();
m1.setId("101");
m1.setName("zhangsan");
m2.setId("102");
m2.setName("lisi");
List<Memeber> objs = new ArrayList<>();
objs.add(m1);
objs.add(m2);
// 写入会员数据
for (int i = 0; i < objs.size(); i++) {
Row r = sheet.createRow(i + 1);
Memeber obj = objs.get(i);
System.out.println("当前数据对象为:" + obj);
r.createCell(0).setCellValue(obj.getId());
r.createCell(1).setCellValue(obj.getName());
r.createCell(2).setCellValue(new SimpleDateFormat("yyyy-MM-dd").format(obj.getRegistDate()));
}
FileOutputStream fo = new FileOutputStream(new File("E:/temp/t2.xls")); // 输出到文件
workbook.write(fo);
fo.close();
}
// 写入对象数据,同时加上标题
@Test
public void insertExcel3() throws Exception {
// 创建工作簿对象(Excel文件)
HSSFWorkbook workbook = new HSSFWorkbook();
// (new FileInputStream(new File("E:/temp/t1.xls")));
// 创建表格中一个sheet对象
HSSFSheet sheet = workbook.createSheet("mysheet2");
// 标头行,代表第一行
HSSFRow
header = sheet.createRow(0);
// 创建单元格,0代表第一行第一列
header.createCell(0).setCellValue("会员编号");
header.createCell(1).setCellValue("会员姓名");
header.createCell(2).setCellValue("注册时间");
Memeber m1 = new Memeber();
Memeber m2 = new Memeber();
m1.setId("101");
m1.setName("lisi");
m2.setId("102");
m2.setName("wang5");
List<Memeber> objs = new ArrayList<>();
objs.add(m1);
objs.add(m2);
// 写入会员数据
for (int i = 0; i < objs.size(); i++) {
Row r = sheet.createRow(i + 1);
Memeber obj = objs.get(i);
System.out.println("当前数据对象为:" + obj);
r.createCell(0).setCellValue(obj.getId());
r.createCell(1).setCellValue(obj.getName());
r.createCell(2).setCellValue(new SimpleDateFormat("yyyy-MM-dd").format(obj.getRegistDate()));
}
FileOutputStream fo = new FileOutputStream(new File("E:/temp/t3.xls")); // 输出到文件
workbook.write(fo);
fo.close();
}
// 读取,全部sheet表及数据
@Test
public void readExcel1() throws Exception {
//绑定输入流生成工作簿对象
HSSFWorkbook workbook = new HSSFWorkbook(new FileInputStream(new File("E:/temp/t3.xls")));
HSSFSheet sheet = null;
//双重
循环读取二维表格
for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
sheet = workbook.getSheetAt(i);// 获取Excel中每个Sheet
for (int j = 0; j < sheet.getLastRowNum() + 1; j++) {
HSSFRow row = sheet.getRow(j);// 获取sheet中每一行
if (row != null) {
for (int k = 0; k < row.getLastCellNum(); k++) {// getLastCellNum,是获取最后一个不为空的列是第几个
if (row.getCell(k) != null) { // getCell 获取每一个单元格数据
System.out.print(row.getCell(k) + "\t");
} else {
System.out.print("\t");
}
}
}
System.out.println(""); // 读完一行后换行
}
System.out.println("读取sheet表:" + workbook.getSheetName(i) + " 完成");
}
}
}
这里我们看到操作中存在一些可以封装的
变化点,主要是用于输出EXCEL的实体对象有关,这些对象实际上是我们的数据源,我们
归纳一下这些变化点及
解决方法:
1.领域对象类型不定--Class 反射
2.领域对象的属性集不定--反射+集合+
泛型
3.输出的Excel表头的标题不定--自定义注解+反射
4.输出的Excel表头(字段)的顺序不定--自定义注解+反射+自定义排序规则
可以看到,封装变化点的主要途径就是结合反射和自定义注解,其中领域对象类型不定和属性集不定属于我们解决方案的基础,可以单纯使用反射解决,后面两个跟业务有一定关联,需要结合自定义注解解决。
——————————————————————————————————————————
根据之前的分析,我们创建一个自定义注解用于封装表格字段标题和字段输出顺序,使这两个变化点可以通用设置值,这就是我们的组件1
1.组件1--自定义注解@ExcelResources 用于接收字段标题和字段输出顺序两个属性值,使用自定义注解的目的是可以
跨实体类通用
/**
* 用来在对象的get方法上加入的annotation,通过该annotation说明某个属性所对应的标题
*
* @author
* @date 2013-7-18
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.FIELD})
public @interface ExcelResources {
/**
* 属性的标题名称
*/
String title();
/**
* 在Excel中的顺序
*/
int order() default 9999;
}
为了实现自定义比较规则,我们创建一个ExcelHeader类实现Comparable接口,将标题、标题顺序、应用自定义注解的方法名抽象出来,将来用于确定字段输出顺序和反射,这就是组件2
2.组件2--ExcelHeader 类,用于设置表头字段的标题和顺序
/**
* 用来存储Excel标题的对象,通过该对象可以获取标题和方法的对应关系
* @author
*
*/
public class ExcelHeader implements Comparable<ExcelHeader>{
/**
* 标题的名称
*/
private String title;
/**
* 每一个标题的顺序
*/
private int order;
/**
* 所对应的的方法名称
*/
private String methodName;
public ExcelHeader(String title, int order, String methodName) {
this.title = title;
this.order = order;
this.methodName = methodName;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getOrder() {
return order;
}
public void setOrder(int order) {
this.order = order;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
/**
* 自定义排序规则
*/
public int compareTo(ExcelHeader o) {
return order>o.order?1:(order<o.order?-1:0);
}
public String toString() {
return "ExcelHeader [title=" + title + ", order=" + order + ", methodName=" + methodName + "]";
}
}
接下来以一个员工实体类作为例子,包含员工的姓名、性别等属性(字段)
3.组件3--员工实体类Employee
public class Employee {
private String empName;//员工姓名
private Integer sex; //1-男 2-女 3-未知
private String idCardNo; //身份证号
private Date regTime;//注册时间
/*
构造器、toString、getters和setters略*/
4.组件4--用于综合使用POI和自定义注解以及测试的类ExcelOperUtil,包含的行为如下
1)Workbook exportObj2Excel(List<?> objs, Class<?> clz, boolean isXssf) 用于创建EXCEL工作簿并返回,objs用于接收实体类集合,作为数据源,clz用于指定实体类类型,作为反射的起点,isXssf用于判断excel文件的版本,具体代码如下:
public Workbook exportObj2Excel(List<?> objs, Class<?> clz
//, boolean isXssf
) throws SecurityException, IllegalArgumentException, NoSuchMethodException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, IOException{
Workbook wb = new XSSFWorkbook();
/*if(isXssf){
wb = new XSSFWorkbook();
}else{
wb = new HSSFWorkbook();
}*/
Sheet sheet = wb.createSheet();
//第一行为表头
Row r = sheet.createRow(0);
//通过传入的实体对象反射出所有属性
List<ExcelHeader> headers = getHeaderList(clz);
//根据实体类各属性的自定义注解值来确定在表格中的序号
Collections.sort(headers);
//写标题
for (int i = 0; i<headers.size(); i++) {
r.createCell(i).setCellValue(headers.get(i).getTitle());
}
//写数据
Object obj = null;
for (int i = 0; i<objs.size(); i++) {
r = sheet.createRow(i+1);
obj = objs.get(i);
System.out.println("当前数据对象为:"+obj);
for (int j = 0; j<headers.size(); j++) {
r.createCell(j).setCellValue(BeanUtils.getProperty(obj, getMethodName(headers.get(j))));
}
}
FileOutputStream fo = new FileOutputStream(new File("E:/temp/t3.xlsx")); // 输出到文件
wb.write(fo);
return wb;
}
?
2)List<ExcelHeader> getHeaderList(Class<?> clz)
私有方法,通过反射得到指定类的ExcelHeader集合,供exportObj2Excel方法调用,完整代码如下:
private List<ExcelHeader> getHeaderList(Class<?> clz){
List<ExcelHeader> headers = new ArrayList<ExcelHeader>();
for(Method m: clz.getDeclaredMethods()){
if(m.getName().startsWith("get")){
if(m.isAnnotationPresent(ExcelResources.class)){
//获取ExcelResources注解对象
ExcelResources er = m.getAnnotation(ExcelResources.class);
//通过实体对象的get方法(间接取得属性名),设置excel表头的列名和列的排列顺序
headers.add(new ExcelHeader(er.title(), er.order(), m.getName()));
}
}
}
return headers;
}
可以看到,这里通过Method类的isAnnotationPresent方法判断业务方法上是否加了ExcelResources自定义注解,在我们的场景中,主要用于业务实体需要导出属性的get方法上。比如一个员工类中相应的属性getters上
@ExcelResources(title = "序号",order = 1)
public Integer getSerialNo() {
return serialNo;
}
3)String getMethodName(ExcelHeader eh)
私有方法,根据标题获取相应的方法名称,供exportObj2Excel方法调用,完整代码为:
private String getMethodName(ExcelHeader eh){
String mn = eh.getMethodName().substring(3);
mn = mn.substring(0,1).toLowerCase()+mn.substring(1);
return mn;
}
4)最后造一个员工类封装成List进行输出EXCEL测试,当然你也可以用其他业务实体输出,只要封装成List传入exportObj2Excel方法即可,灵活通用,核心代码如下:
Workbook wb = util.exportObj2Excel(employeeList, Employee.class);
System.out.println("生成的工作簿:"+wb);
?
总结:本文组合使用了POI,自定义注解,Java反射等知识,通过一个实例,展示了如何实现不同业务对象输出EXCEL的通用解决方案。并且从设计的角度,
发现问题、分析问题、解决问题,逐步演化我们的设计,所有源码完整上传到附件中,供大家参考。