欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java自定义注解以及在POI导出EXCEL中的一个应用 javaannotationPOIexcel 

程序员文章站 2022-07-13 22:53:54
...

      本文简介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方法。该方法会从memberValues这个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的通用解决方案。并且从设计的角度,发现问题、分析问题、解决问题,逐步演化我们的设计,所有源码完整上传到附件中,供大家参考。