注解定义
注解其实是一种代码辅助工具,它的核心作用是对类、方法、变量、参数和包进行标注。程序员可以通过反射或者AOP(底层也是反射),获取这些标注信息,从而在运行时改变所注解对象的行为。比如可以对一个方法的调用前后做相应的业务处理等操作。Java注解由内置注解和元注解组成。
注解与注释的区别
- 注释主要用于代码含义的描述。在编译后的class文件中是不存在的。
- 注解用于对类、方法、变量、参数和包等信息的标注,根据需要可以保存到class文件中,甚至运行期加载的class对象中。
注解基本组成
创建格式
public @interface 注解名 { }
注解核心要素:元注解
1、 @Retention 定义注解的生命周期:【source -> class ->runtime,一般都使用runtime,表示在运行时存在】
2、 @Documented 文档注解,会被javadoc工具文档化
3、 @inherited 是否让子类继承该注解
4、 @Target 描述了注解的应用范围 【常用的有 -> TYPE :类、解耦、注解、枚举;METHOD:方法;FIELD:属性或枚举常量 】
注解基础案例
根据上面的定义,我们可以尝试定义一个注解@Study,它的生命周期runtime,即运行时存在,应用范围为FIELD和TYPE,因此,它可以放在属性字段或者类上,如Person类所示:
@Target({ElementType.FIELD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Study {
}
@Study
public class Person {
private String name;
@Study
private int age;
}
现在的@Study就是一个最简单的注解。由定义我们可以知道,注解其实就是用来标注信息的,而Java支持注解本身携带一些基本类型的基础信息,从而使得注解能够被更加灵活地使用。现在我们可以在@Study这个注解中,定义两个属性:一个是name,它有默认值 hejianlin;一个是String类型的mores数组,它需要在使用注解时传入。
@Target({ElementType.FIELD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Study {
//可以包含基本属性,可以定义默认值
String name () default "hejianlin";
//这里没有定义默认值,表示在使用注解时需要传入,否则将报错
String[] mores ();
}
//由于name有默认值,所以这里可以不用传入,mores不传,则会报错
@Study(mores = {"liLei","liMei"})
public class Person {
private String name;
@Study(mores = {"liBai","liHei"},name = "liTie")
private int age;
}
到这里为止,注解其实只是对相应的类或属性做了标注,而我们如何在运行时,对我们所标注的类或属性,实现额外的、自定义的业务呢?这里就涉及到了反射。
反射定义
在运行状态中,能够动态地获取类的所有属性和方法,能够调用任意一个对象的任意一个方法和属性,这种功能就是反射。
常用的反射API
反射基础案例
public class ReflectionDemo {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException,
InstantiationException, NoSuchMethodException, InvocationTargetException {
System.out.println("示例1: 通过反射获取class元信息");
Person person = new Person();
//通过对象反射得到元信息
Class<? extends Person> classByObject = person.getClass();
//通过类名反射得到元信息
Class<?> classByName = Class.forName("com.hejianlin.model.Person");
System.out.println("两种方式得到的元信息:"+classByObject+","+classByName+";是否相同:"+classByName.equals(classByObject));
System.out.println("示例2: 通过反射获取类名、包名");
String name = classByName.getName();
String simpleName = classByName.getSimpleName();
System.out.println("全限制类名:"+name);
System.out.println("类名:"+simpleName);
System.out.println("示例3: 获取类属性");
Field[] declaredFields = classByName.getDeclaredFields();
for (Field declaredField : declaredFields) {
System.out.println("类属性:"+declaredField);
}
System.out.println("示例4:获取类属性具体值");
person.setName("jianlin");
person.setAge(18);
for (Field declaredField : declaredFields) {
//设置属性可见,从而可以得到一些私有属性的值
declaredField.setAccessible(true);
System.out.println("属性:"+declaredField+",值:"+declaredField.get(person));
}
System.out.println("示例5:反射实例化对象");
Object object = classByName.newInstance();
for (Field declaredField : declaredFields) {
declaredField.setAccessible(true);
//判断属性名是否是name
if(declaredField.getName().equals("name")){
//设置name的属性值
declaredField.set(object,"xiaohong");
}
if(declaredField.getName().equals("age")){
//设置name的属性值
declaredField.set(object,28);
}
System.out.println("属性:"+declaredField+",值:"+declaredField.get(object));
}
System.out.println("示例6:反射获取当前类的方法");
Method[] methods = classByName.getMethods();
for (Method method : methods) {
System.out.print(method.getName()+",");
}
System.out.println();
Method getToMethod = classByName.getMethod("toString");
Object value = getToMethod.invoke(object);
System.out.println("toString方法返回值:"+value);
}
}
通过运行这个main方法,我们就可以了解如何在运行时动态获取类的所有属性和方法,结果如下:
示例1: 通过反射获取class元信息
两种方式得到的元信息:class com.hejianlin.model.Person,class com.hejianlin.model.Person;是否相同:true
示例2: 通过反射获取类名、包名
全限制类名:com.hejianlin.model.Person
类名:Person
示例3: 获取类属性
类属性:private java.lang.String com.hejianlin.model.Person.name
类属性:private int com.hejianlin.model.Person.age
示例4:获取类属性具体值
属性:private java.lang.String com.hejianlin.model.Person.name,值:jianlin
属性:private int com.hejianlin.model.Person.age,值:18
示例5:反射实例化对象
属性:private java.lang.String com.hejianlin.model.Person.name,值:xiaohong
属性:private int com.hejianlin.model.Person.age,值:28
示例6:反射获取当前类的方法
equals,toString,hashCode,getName,setName,setAge,getAge,wait,wait,wait,getClass,notify,notifyAll,
toString方法返回值:Person(name=xiaohong, age=28)
注解+反射的使用
首先,我们可以在运行时,通过对象的类类型,获取类上所带的注解,解析注解所携带的信息,还是以上面的ReflectionDemo类为例,代码片段如下:
System.out.println("示例7: 获取注解");
Study study = classByName.getAnnotation(Study.class);
String[] mores = study.mores();
String name1 = study.name();
System.out.println("从注解获取的属性值:"+name1+","+mores);
同理,获取方法或属性上的注解,可通过反射得到的Method对象或Field对象,调用getAnnotation方法获取。
注解+反射案例:模拟类对象到sql语句的转化
假设我们现在有张订单表,Java与数据库表映射时,需要建立对应的实体类,由于数据库表名、字段名的语法格式与Java类名、类属性的语法格式不同。我们可以建立相应的注解进行映射。订单类Order如下:
@SqlState("t_order")
@Data
public class Order {
/**
* id
*/
@Column //没有指定字段名,则默认和属性名相同
private Long id;
/**
* 订单号
*/
@Column("order_no")
private Long orderNo;
/**
* 订单名称
*/
@Column("order_name")
private String orderName;
/**
* 商店id
*/
@Column("shop_id")
private Long shopId;
/**
* 用户id
*/
@Column("user_id")
private Long userId;
}
这里用到的@SqlState、@Column等注解,就是我们自定义的映射表名、字段名的注解,如下所示:
/**
* 表名注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SqlState {
String value();
}
/**
* 字段注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
String value() default "";
}
试想一下,我们平常日常开发过程中,用的最多的就是单表查询,单表查询的sql语句不外乎
select 字段名 from 表名 where 条件
这种格式吧,而这里的字段名、表名、条件什么的,其实都是可以通过java对象的属性名、类名等反射出来的。所以通过反射,我们就可以动态地拼装单表查询sql语句。mybatis plus其实也是通过反射的机制来实现功能的。
假设我们现在需要根据订单名称和商店id查询对应的订单记录:
public class AppTest
{
@Test
public void testSql() throws Exception {
Order order = new Order();
order.setShopId(101L);
order.setOrderName("维他柠檬茶");
String querySql = GenerateSqlUtil.query(order);
System.out.println("查询sql:"+querySql);
}
}
我们先实例化相应的订单对象,设置商店id和订单名称字段的值,然后调用GenerateSqlUtil.query方法,实现动态sql语句的拼装。GenerateSqlUtil.query方法方法实现如下:
public class GenerateSqlUtil {
/**
* sql语句生成器:select 字段名 from 表名 where 条件
* @param tableObject
* @return
* @throws Exception
*/
public static String query(Object tableObject) throws Exception{
//select 语句
StringBuffer sb = new StringBuffer();
sb.append("select");
StringBuffer whereSb = new StringBuffer();
whereSb.append(" 1=1 ");
//获取类类型
Class<?> clazz = tableObject.getClass();
//获取表名注解
SqlState sqlState = clazz.getAnnotation(SqlState.class);
if(sqlState == null){
throw new RuntimeException("表名映射失败!");
}
//获取属性
Field[] declaredFields = clazz.getDeclaredFields();
//获取表名
String tableName = sqlState.value();
//获取select语句
String selectSqlStr = generateSelectSql(declaredFields);
//获取where语句
String whereSqlStr = generateWhereSql(declaredFields, tableObject);
return selectSqlStr+" from "+tableName+" "+whereSqlStr+" ;";
}
/**
* 生成select语句
* @param declaredFields
* @return
*/
private static String generateSelectSql(Field[] declaredFields){
StringBuffer sb = new StringBuffer();
sb.append("select");
for (Field field : declaredFields) {
//获取字段名注解
Column column = field.getAnnotation(Column.class);
if(column == null){
throw new RuntimeException("字段名映射失败:"+field.getName());
}
String columnName = column.value();
if(columnName.equals("")){
//默认使用属性名
sb.append(" "+field.getName()+",");
}else{
//使用字段名
sb.append(" "+columnName+",");
}
}
//去除末尾的逗号
sb.deleteCharAt(sb.length()-1);
return sb.toString();
}
/**
* 生成where语句
* @param declaredFields
* @param tableObject
* @return
* @throws Exception
*/
private static String generateWhereSql(Field[] declaredFields,Object tableObject) throws Exception {
StringBuffer sb = new StringBuffer();
sb.append("where 1=1");
for (Field field : declaredFields) {
//获取字段名注解
Column column = field.getAnnotation(Column.class);
if(column == null){
throw new RuntimeException("字段名映射失败:"+field.getName());
}
String columnName = column.value();
String name = field.getName();
field.setAccessible(true);
Object columnValue = field.get(tableObject);
if(columnValue == null ){
continue;
}
//如果是字符串类,则添加单引号
if(columnValue instanceof String){
columnValue="'" + columnValue + "'";
}
if(columnName.equals("")){
//默认使用属性名
sb.append(" and "+name+" = "+columnValue);
}else{
//使用字段名
sb.append(" and "+columnName+" = "+columnValue);
}
}
return sb.toString();
}
}
sql语句主要由三部分组成,select语句、表名、where语句,因此各自封装成一个方法,最后拼接在一起就可以了。执行上面的测试类,输出如下的sql语句:
查询sql:select id, order_no, order_name, shop_id, user_id from t_order where 1=1 and order_name = '维他柠檬茶' and shop_id = 101 ;
至此,简单的sql语句通过反射的方式就成功生成了。当然这只是简单示例,如果有兴趣的话,可以阅读mybatis plus的源码,了解如何反射更加复杂的sql语句。
最后,源代码传送门,有需要的请点击,谢谢!
##