扩展Mybatis,免配XML文件自动实现增删改和随意查询

写在前面

以前有点懒,一直没有分享技术新的的习惯,这次居然有心情写点东西分享出来。
作为一个从很老很老的jdk就开始码java代码的码农,对Mybatis却一直都是一无所知,一直固守在Hibernate持久层框架上。虽然这个红的发紫的MyBatis 是一款优秀的持久层框架,而我居然都没有了解过,也没有用过。想想也觉得汗颜呀。
言归正传,现在切入正题。
还有需要先申明一下:本文档为本人原创,欢迎引用,本着方便字节,也方便大家的原则,独乐乐,众乐乐,不若与众的精神,只有能给大家工作带来用处,本老农就心满意足了。

背景,为什么要学习

因项目需要,决定采用springboot+Mybatis。于是,我也加入了这个码农圈无人不知,无人不晓的Mybatis开发大队伍里。据说:这个可是各个大厂的御用持久层技术框架,一时用一时爽,一直用就一直爽;还听说,Mybatis能够让你随心所欲写SQL。
以前使用hibernate,对hibernate的一些使用习惯还是念念不忘,总想Mybatis能有该多好。真的是如假包换,百分百的hibernate的添狗一只。
因项目,在这个项目开始前开始学习Mybatis,翻资料,网上找例子,就差去整一本《二十一天精通Mybatis》来了。不过,从开始学习,接触Mybatis开始算起,已经2周左右了,马上就差不多要到21天了,感觉离精通还有十万八千里路远。
就这样子,还班门弄斧,各位看官,莫笑。如不听劝,牙齿掉了,还请自负其责,自费补牙。

Mybatis的典型规范

典型的规范:

  1. 写个实体EntityBean类,描述数据库表或查询结构之间的映射关系。
@Entity
@Table(name = "User")
public class User{
   @Id
   private int id;
   private String name;

   ……

} 
  1. 写个Mapper接口,定义访问数据库表的方法。Mybatis采用JDK本身的接口动态代理实现。
public interface UserMapper {  
    //根据用户id查询用户信息  
    public User findUserById(int id) throws Exception;  
    //查询用户列表  
    public List<User> findUserByUsername(String username) throws Exception;  
    //添加用户信息  
    public void insertUser(User user)throws Exception;   

    ……

}
  1. 写一个.XML文件,配置Mapper接口方法所需要的SQL语句,为了实现一定的动态能力,还定义了一套XML标签来标记条件、循环等。
<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE mapper  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"  
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">  
<mapper namespace="com.xxx.UserMapper">  
<!-- 注意这里的 namespace必须对应着map接口的全类名-->  
    <select id="findUserById" parameterType="int" resultType="user">  
        select * from user where id = #{id}  
    </select>  

    <select id="findUserByUsername" parameterType="java.lang.String"  
        resultType="user">  
        select * from user where username like '%${value}%'  
    </select>  

    <insert id="insertUser" parameterType="user">  
        <selectKey keyProperty="id" order="AFTER" resultType="java.lang.Integer">  
            select LAST_INSERT_ID()  
        </selectKey>  
        insert into user(username,birthday,sex,address)  
        values(#{username},#{birthday},#{sex},#{address})  
    </insert>  
……
</mapper>  

为了写好这三个文件,还有大拿提供了专门的脚手架插件来完成这个事情,不过,为了与前端页面配合,编辑调试XML的工作还是有不少的。

懒人的想法

1.以前用Hibernate时,只定义了一个基础接口,把常用的增删改查放在基类接口中,在扩展接口中定义少量的个性化方法,利用注解的方式和JDK和CGLIB的动态代理机制,实现接口功能。Mybatis能不能也像Hibernate一样,直接定义个通用接口就好了,再也不用配置XML或注解SQL。减少码代码的工作量和测试工作量。
2.Mybatis分页方式,大多采用应用层分页方式实现,应用层分页,数据会从数据库服务器向应用服务器传输。当数据集特别大,比如十万级百万级甚至千万级时,数据从数据库服务器向应用服务器传输,给网络带来了非常大的压力。那能不能实现任意字段方式的查询,以及真正的数据库端的分页呢?
说干就干,本着辛苦我一人,造福兄弟伙的国际奉献主义精神,我不当垫脚石谁来当垫脚石的牺牲精神。我这个21天都不到的Mybatis的婴儿码农脱了衣服,再撸起袖子加油干。

突破口在哪儿

翻了一堆的资料,找下手的突破口。功夫不负我心,找到了两个下手地。首先,我来看看Mybatis的执行过程。

1.Mybatis提供有方法拦截器的切入口。Mybatis支持对Executor、StatementHandler、PameterHandler和ResultSetHandler 接口的方法进行拦截,也就是说会对这4类对象的方法进行代理拦截。

在mybatis中可被拦截的类型有四种(按照拦截顺序):

  1. Executor:拦截执行器的方法。
  2. ParameterHandler:拦截参数的处理。
  3. ResultHandler:拦截结果集的处理。
  4. StatementHandler:拦截Sql语法构建的处理。

2.针对动态拼接SQL, Mybatis3中增加了使用注解来配置Mapper的新特性,我们主要用到@SelectProvider、@UpdateProvider、@InsertProvider和@DeleteProvider几个注解来实现。
有了这些注解,就可以让我们在运行时将SQL构造出来。实现动态SQL生成,处理数据。换句话说,就是可以根据传进来的实体类型及字段值,就可以组装出你想要的SQL。这样做是不是很爽。
有了这两个切入点,我们就可以在完全不破坏Mybatis架构的前提下,把我们的私心暗恋想法悄无声息地注入进去。光想一下都让我有点鸡冻不已。激动让我语无伦次,必须马上动手实现它。不能光临渊羡鱼,还得退而结网
让我们来看看下面这幅图,我们要下手的地方在哪儿。

在这里插入图片描述

进入正题

为了少写点代码,把经常用到的通用方法放在基类接口中,不使用XML,子类接口直接继承就好了。
废话就不BB了,直接上代码:
由于文档篇幅,下面提供主要类的部分代码片段。进行示例。

A. 基类接口

定义基类接口,基类接口实现 
1.数据处理操作,包括insert,update,delete,save(自动判断使用insert或update)方法,
2.数据库查询操作,包括分页查询。查询条件参数支持map、dto对象,EntityBean对象
Mapper接口泛型GenericMapper,提供子类对具体实体类的支持。

public interface GenericMapper<T> {
	@SelectProvider(type = GenericSelectProvider.class, method = "getById")
	T getById(@Param("entityClass") Class<T> entityClass, @Param("entityId") Object params);
	
	@SelectProvider(type = GenericSelectProvider.class, method = "query")
	List<T> query(@Param("entityClass") Class<T> entityClass,@Param("params") Map params,String orderBy);
	@SelectProvider(type = GenericSelectProvider.class, method = "pagedQuery")
	Paginate<T> pagedQuery(@Param("entityClass") Class<T> entityClass,@Param("paginate") Paginate<T> paginate,@Param("params") Map params,String orderBy);

	@SelectProvider(type = GenericSelectProvider.class, method = "count")
	int count(@Param("entityClass") Class<T> entityClass,@Param("params") Map params);
	
	@SelectProvider(type = GenericSelectProvider.class, method = "queryByDto")
	List<T> queryByDto(@Param("entityClass") Class<T> entityClass,@Param("params") Object params,String orderBy);

	@SelectProvider(type = GenericSelectProvider.class, method = "queryByFilterRules")
	List<T> queryByFilterRules(@Param("entityClass") Class<T> entityClass,@Param("filterRules") List<DataFilterRule> filterRules,String orderBy);

	@InsertProvider(type = GenericSaveProvider.class, method = "insert")
	Integer insert(@Param("entityBean") T entityBean);

	@UpdateProvider(type = GenericSaveProvider.class, method = "update")
	Integer update(@Param("entityBean") T entityBean);
	
	@DeleteProvider(type = GenericSaveProvider.class, method = "delete")
	Integer remove(@Param("entityBean") T entityBean);
	
	@UpdateProvider(type = GenericSaveProvider.class, method = "save")
	Integer save(@Param("entityBean") T entityBean);

}
B. 提供Mybatis使用的Providor

Providor提供动态sql切入点。GenericSelectProvidor提供实体类或查询对象的动态数据查询的切入点,GenericSaveProvidor提供对通用实体类的动态增删改操作。

1. GenericSelectProvidor代码
public class GenericSelectProvider {
	
	private final Logger logger = LoggerFactory.getLogger(this.getClass());	
    protected static final String SQL_END_DELIMITER = ";";  

	@SelectProvider(type = GenericSelectProvider.class, method = "getById")
	public <T> String getById(@Param("entityClass") Class<T> entityClass, @Param("entityId") Object params){
		EntityBeanInfo dmi=EntityBeanInfo.getDtoMaperInfo(entityClass);
		if (dmi==null){
			throw new RuntimeException("不是数据库映射对象");
		}
		String sql=dmi.getGetByIdSQL();
		return sql;
	}
    
    
	public <T> String query(@Param("entityClass") Class<T> entityClass, @Param("params") Map<String,Object> params,String orderBy){
		EntityBeanInfo dmi=EntityBeanInfo.getDtoMaperInfo(entityClass);

		if (dmi==null){
			throw new RuntimeException("不是数据库映射对象");
		}
		
		String sql=dmi.getSelectSQL();		
		
		Map<String,ColumnInfo> columns=dmi.getColumns();
		String whereClause = whereClause(columns,params);		
		String _orderBy = orderByClause(dmi.getColumns(),orderBy);
		
		if (whereClause!=null && whereClause.length()>0){
			sql=insertWhereClause(sql,whereClause);
		}
		
		if (_orderBy!=null && _orderBy.length()>0){
			sql=sql+"\r\n order by " + _orderBy;
		}
		
        logger.debug("mybatis get query sql:{}", sql);
		return sql;
	}
	
	private String insertWhereClause(String sql,String whereClause){
		int wherePos=SqlUtil.indexOfKeywordInSql(sql, "where");
		if (wherePos<1){
			sql=sql+"\r\n where " + whereClause;				
		}else{
			int groupPos = SqlUtil.indexOfKeywordInSql(sql, "group", wherePos + 5);
			int orderPos = SqlUtil.indexOfKeywordInSql(sql, "order", wherePos + 5);
			int insertPos = wherePos < groupPos ? groupPos : wherePos;
			insertPos = insertPos < orderPos ? orderPos : insertPos;
			if (insertPos != wherePos) {
				sql = sql.substring(0, insertPos) + "\r\n and " + whereClause + "\r\n" + sql.substring(insertPos);
			} else {
				sql = sql + "\r\n and " + whereClause;
			}
		}
		return sql;
		
	}
	
	public <T> String pagedQuery(@Param("entityClass") Class<T> entityClass,@Param("paginate") Paginate<T> paginate,
			@Param("params") Map params,String orderBy){
		EntityBeanInfo dmi=EntityBeanInfo.getDtoMaperInfo(entityClass);
		if (dmi==null){
			throw new RuntimeException("不是数据库映射对象");
		}
				
		Map<String,ColumnInfo> columns=dmi.getColumns();
		String whereClause = whereClause(columns,params);		
		String _orderBy = orderByClause(dmi.getColumns(),orderBy);
		
		String slctSql=dmi.getSelectSQL();		
		if (whereClause!=null && whereClause.length()>0){
			slctSql=insertWhereClause(slctSql,whereClause);
		}
		
		if (_orderBy!=null && _orderBy.length()>0){
			slctSql=slctSql+"\r\n order by " + _orderBy;
		}
		
        logger.debug("GenericSelectProvider.pagedquery() sql:{}", slctSql);
        //System.out.println("slctsql:"+slctSql);
		return slctSql;		
	}
	
	public <T> String count(@Param("entityClass") Class<T> entityClass, @Param("params") Map<String,Object> params){
		EntityBeanInfo dmi=EntityBeanInfo.getDtoMaperInfo(entityClass);

		if (dmi==null){
			throw new RuntimeException("不是数据库映射对象");
		}
		
		String sql=dmi.getCountSQL();		
		
		Map<String,ColumnInfo> columns=dmi.getColumns();
		String whereClause = whereClause(columns,params);		
		
		if (whereClause!=null && whereClause.length()>0){
			sql=sql+"\r\n where " + whereClause;
		}
				
        logger.debug("GenericSelectProvider.count() sql:{}", sql);
		//System.out.println("count sql:"+sql);
		return sql;
	}

	private String whereClause(Map<String,ColumnInfo> columns, Map<String,Object> params){
		StringBuffer whereClause=new StringBuffer();
		params.forEach((paraName,paraValue)->{
			getCondition(paraName,paraValue,columns,whereClause);
		});
		return whereClause.toString();
	}

	public <T> String queryByDto(@Param("entityClass") Class<T> entityClass,@Param("params") Object params,String orderBy){
		EntityBeanInfo dmi=EntityBeanInfo.getDtoMaperInfo(entityClass);

		if (dmi==null){
			throw new RuntimeException("不是数据库映射对象");
		}
		
		String sql=dmi.getSelectSQL();		
		
		Map<String,ColumnInfo> columns=dmi.getColumns();
		String whereClause = whereClause(columns,params);
		String _orderBy = orderByClause(dmi.getColumns(),orderBy);

		if (whereClause!=null && whereClause.length()>0){
			sql=insertWhereClause(sql,whereClause);
		}
		if (_orderBy!=null && _orderBy.length()>0){
			sql=sql+"\r\n order by " + _orderBy;
		}
		
        logger.debug("GenericSelectProvider.queryByDto() sql:{}", sql);
		return sql;		
	}

	private String whereClause(Map<String,ColumnInfo> columns, Object params){
		StringBuffer whereClause=new StringBuffer();
		Field[] fields=params.getClass().getDeclaredFields();
		String paraName;
		Object paraValue;
		for(Field field:fields){
			paraName=field.getName();
			try {
				paraValue=BeanUtils.forceGetProperty(params, paraName);
				if (paraValue==null)
					continue;
			} catch (NoSuchFieldException e) {
				continue;
			}
			getCondition(paraName,paraValue,columns,whereClause);
		}
		return whereClause.toString();
	}
	
	public <T> String queryByFilterRules(@Param("entityClass") Class<T> entityClass, @Param("filterRules") List<DataFilterRule> filterRules,String orderBy){
		EntityBeanInfo dmi=EntityBeanInfo.getDtoMaperInfo(entityClass);

		if (dmi==null){
			throw new RuntimeException("不是数据库映射对象");
		}
		
		String sql=dmi.getSelectSQL();		
		
		Map<String,ColumnInfo> columns=dmi.getColumns();
		String whereClause = whereClause(columns,filterRules);
		String _orderBy = orderByClause(dmi.getColumns(),orderBy);

		if (whereClause!=null && whereClause.length()>0){
			sql=insertWhereClause(sql,whereClause);
		}
		
		if (_orderBy!=null && _orderBy.length()>0){
			sql=sql+"\r\n order by " + _orderBy;
		}
		
        logger.debug("GenericSelectProvider.queryByFilterRules() sql:{}", sql);
		return sql;
		
	}
	private String whereClause(Map<String,ColumnInfo> columns, List<DataFilterRule> filterRules){
		StringBuffer whereClause=new StringBuffer();
		String paraName;
		Object paraValue;
		for(DataFilterRule rule:filterRules){
			paraName=rule.getField();
			paraValue=rule.getValue();
			if (paraValue==null)
				continue;
			getCondition(paraName,paraValue,columns,whereClause);
		}
		return whereClause.toString();
	}
	
	
	private void getCondition(String paraName,Object paraValue,Map<String,ColumnInfo> columns,StringBuffer whereClause){
		
		//基本条件:属性等于属性值
		String field,queryMode;
		ColumnInfo column=columns.get(paraName);
		Class<?> type;
		if (column != null){
			if (!column.isFilterable()){
				return;
			}
			if (whereClause.length()>0)
				whereClause.append(" and ");
			type=column.getJavaType();
			whereClause.append(column.getFilterName()).append(" = #{params.").append(paraName).append("}");
			return ;
		}
		
		//复杂条件:参数为 属性名_条件类型
		//条件类型为:包含,多值,大于,小于等。
		int pos;
		pos=paraName.lastIndexOf("_");
		if (pos<0){
			//单值查询
			field=paraName;
			queryMode="";
		}else{
			field=paraName.substring(0,pos);
			queryMode=paraName.substring(pos+1);			
		}
		column=columns.get(field);
		if (column==null || !column.isFilterable())
			return;
		type=column.getJavaType();

		if (queryMode.indexOf("multiple")<0){
			if (paraValue==null) 
				return;
			if ((paraValue instanceof String) && ((String )paraValue).trim().length()==0 )
				return;
			getCondition(column.getFilterName(),paraName,queryMode,type,paraValue,whereClause);
		}else{
			if (paraValue instanceof Collection) {
				String value="";
				for(Object str:((Collection<?>)paraValue)){
					value+=str+",";
				}
				getCondition(column.getFilterName(),paraName,queryMode,type,value.substring(0,value.length()-1),whereClause);	
			}
		}
	}
	private void getCondition(String field,String paraName,String queryMode,Class<?> type,Object value,StringBuffer whereClause){
		if (queryMode.length()==0){
			if (whereClause.length()>0)
				whereClause.append(" and ") ;
				whereClause.append(field).append(" = #{params.").append(paraName).append("}");
		}else{
			if ("begin".equalsIgnoreCase(queryMode)){
				if (whereClause.length()>0)
					whereClause.append(" and ") ;
					whereClause.append(field).append(" >= #{params.").append(paraName).append("}");
			}
			if ("end".equalsIgnoreCase(queryMode)){
				if (whereClause.length()>0)
					whereClause.append(" and ") ;
					whereClause.append(field).append(" <= #{params.").append(paraName).append("}");
			}
			if ("like".equalsIgnoreCase(queryMode)){
				if (whereClause.length()>0)
					whereClause.append(" and ") ;
					whereClause.append(field).append(" like concat(#{params.").append(paraName).append("},'%')");
			}
			if ("likeAll".equalsIgnoreCase(queryMode)){
				if (whereClause.length()>0)
					whereClause.append(" and ") ;
					whereClause.append(field).append(" like concat('%',#{params.").append(paraName).append("},'%')");
			}
			//多值in查询
			if ("multiple[]".equalsIgnoreCase(queryMode)){
				createInClause(field,type,value,whereClause);
			}
		}
	}
	protected String castType(Class<?> type,Object value){
		if (type==null){
			return value.toString();
		}
		if (type == String.class){
			return "'"+value+"'";
		}
		if (type == Boolean.class || type == boolean.class){
			return 	"'"+value+"'";
		}
		if (type == java.sql.Date.class){
			return "'"+value+"'";
		}
		if (type == java.util.Date.class){
			return "'"+value+"'";
		}
		if (type == Timestamp.class){
			return "'"+value+"'";
		}
		if (type == Integer.class || type == int.class){
			return value.toString();
		}
		
		if (type == Long.class || type == long.class){
			return value.toString();
		}
		if (type == Float.class || type == float.class){
			return value.toString();
		}
		if (type == Double.class || type == double.class){
			return value.toString();
		}
		return value.toString();

	}
	//构造多值IN查询
	protected void createInClause(String field,Class<?> type,Object value,StringBuffer whereClause){
		String[] strs=value.toString().split("\r\n|\r|\n| |,");
		StringBuffer sb=new StringBuffer();
		for (String str:strs){
			str=str.trim();
			if (str.length()==0)
				continue;
			str=str.replace("'", "''");//转义单引号,预防SQL注入
			sb.append(castType(type,str)+",");
		}
		if (sb.length()>1){
			if (whereClause.length()>0)
				whereClause.append(" and ") ;
			whereClause.append(field).append(" in (").append(sb.substring(0,sb.length()-1)).append(")");
		}
	}
	
	public String orderByClause( Map<String, ColumnInfo> columns,String orderBy){
		if (orderBy==null)
			return "";
		StringBuffer orderByClause=new StringBuffer();
		String[] orderProps=orderBy.trim().split(",");
		for(String op : orderProps){
			if (op==null)
				continue;
			orderByClause(columns,op.trim(),orderByClause);
		}
		return orderByClause.toString();
	}
	private void orderByClause(Map<String, ColumnInfo> columns,String orderBy,StringBuffer orderByClause){
		int pos;
		String propName,orderType;
		pos=orderBy.indexOf(" ");
		if (pos>1){
			propName=orderBy.substring(0,pos);
			orderType=orderBy.substring(pos+1).toLowerCase();
		}else{
			propName=orderBy;
			orderType="asc";
		}
		ColumnInfo columnInfo=columns.get(propName);
		if (columnInfo!=null){
			if (orderByClause.length()>0)
				orderByClause.append(",");
			orderByClause.append(columnInfo.getColumnName()).append(" ").append(orderType);
		}
	}
}
2. GenericSaveProvidor代码
public class GenericSaveProvider {
	
	public static String SAVE_ENTITY_BEAN="save";
	public static String SAVE_OR_UPDATE_ENTITY_BEAN="saveOrUpdate";
	
	public <T>  String update(@Param("entityBean") T entityBean){
		EntityBeanInfo dmi=EntityBeanInfo.getDtoMaperInfo(BeanUtils.getEntityClass(entityBean));
		String sql=dmi.getUpdateSQL();
		return sql;
	}
	public <T> String insert(@Param("entityBean") T entityBean){

		EntityBeanInfo dmi=EntityBeanInfo.getDtoMaperInfo(BeanUtils.getEntityClass(entityBean));		
		return dmi.getInsertSQL();
	}
	public <T> String delete(@Param("entityBean") T entityBean){

		EntityBeanInfo dmi=EntityBeanInfo.getDtoMaperInfo(BeanUtils.getEntityClass(entityBean));		
		String sql=dmi.getDeleteSQL();
		return sql;
	}
	public <T> String save(@Param("entityBean") T entityBean){

		EntityBeanInfo dmi=EntityBeanInfo.getDtoMaperInfo(BeanUtils.getEntityClass(entityBean));		
		String sql=dmi.getSaveSQL();
		return sql;
	}
}
C. 实体EntityBean描述类EntityBeanInfo

EntityBeanInfo用以描述bean信息,这个类既可以描述有@Entity和@Table注解的实体对象,也可以描述有自定义注解@BeanSQLMap注解的查询结果对象。他缓存了用于查询和修改实体对象的基本的sql语句。
用于描述EntityBean字段与数据库映射的ColumnInfo类。ColumnInfo描述了字段的属性名,读取数据时数据库字段名,查询数据库时的字段名,是否可以用于查询等,供生产动态SQL时使用。

1. EntityBeanInfo类
public class EntityBeanInfo implements Iterable<Map.Entry<Class<?>, EntityBeanInfo>>{
	public static Map<Class<?>, EntityBeanInfo> entityBeanInfos=new HashMap<>();
	
	private Class<?> entityClass;
	private Method[] methods;
	private Field[] fields;
	
	private BeanMappingType dataObjectType=BeanMappingType.Table;
	private String tableName;
	private String getByIdSQL;
	private String selectSQL;
	private String countSQL;
	private String existSQL;
	private String insertSQL;
	private String updateSQL;
	private String deleteSQL;
	private String saveSQL;
	
	private boolean hasWhereClause = false;
	
	private Map<String,ColumnInfo> columns=new HashMap<>();
	private List<ColumnInfo> IdColumns=new ArrayList<>();
	
	public static EntityBeanInfo getDtoMaperInfo(Class<?> entityClass){
		EntityBeanInfo beanInfo=entityBeanInfos.get(entityClass);
		if (beanInfo==null)
			beanInfo=createDtoMaperInfo(entityClass);
		return beanInfo;
	}
	
	public static EntityBeanInfo createDtoMaperInfo(Class<?> entityClass){
		EntityBeanInfo dmi=new EntityBeanInfo(entityClass);
		entityBeanInfos.put(entityClass, dmi);
		return dmi;
	}

	private EntityBeanInfo(Class<?> entityClass){
		this.entityClass=entityClass;
		getFields();	
		getMethods();
		getTableInfo();
		createColumns();
		generateIdList();
		createCountSQL();
		createSelectSQL();
		createInsertSQL();
		createUpdateSQL();
		createDeleteSQL();
		createGetByIdSQL();
		createExistSQL();
		createSaveSQL();
	}
	
	private void getFields(){
		Field[] newFieldArr=new Field[0];
		Field[] FieldArr,oldFieldArr;
		for (Class<?> superClass = entityClass; superClass != Object.class; superClass = superClass.getSuperclass()) {
			FieldArr=superClass.getDeclaredFields();
			oldFieldArr=newFieldArr;
			newFieldArr=new Field[oldFieldArr.length+FieldArr.length];
			System.arraycopy(oldFieldArr, 0, newFieldArr, 0, oldFieldArr.length);
			System.arraycopy(FieldArr, 0, newFieldArr, oldFieldArr.length, FieldArr.length);
		}
		fields=newFieldArr;
	}
	private void getMethods(){
		methods=entityClass.getMethods();
		Method[] newMethodArr=new Method[0];
		Method[] methodArr,oldMethodArr;
		for (Class<?> superClass = entityClass; superClass != Object.class; superClass = superClass.getSuperclass()) {
			methodArr=superClass.getDeclaredMethods();
			oldMethodArr=newMethodArr;
			newMethodArr=new Method[oldMethodArr.length+methodArr.length];
			System.arraycopy(oldMethodArr, 0, newMethodArr, 0, oldMethodArr.length);
			System.arraycopy(methodArr, 0, newMethodArr, oldMethodArr.length, methodArr.length);
		}
		methods=newMethodArr;
	}
	
	private void getTableInfo(){
		//对象映射到SQL
		BeanSQLMap sqlmap=entityClass.getAnnotation(BeanSQLMap.class);
		if (sqlmap != null ){
			String mappingSQL=sqlmap.mappingSQL();
			if (mappingSQL!=null && mappingSQL.trim().length()>0){
				selectSQL=mappingSQL;
				dataObjectType=BeanMappingType.Select;
				hasWhereClause=sqlmap.hasWhereClause();
			}
			countSQL=sqlmap.countSQL();
			return;
		}
		
		//对象映射到数据库表
		Table table=entityClass.getAnnotation(Table.class);
		if (table!=null){
			tableName=table.name();
			dataObjectType=BeanMappingType.Table;
		}
		
	}
	
	public List<ColumnInfo> getIdColumns() {
		return IdColumns;
	}
	
	private void generateIdList(){
		columns.forEach((field,column)->{
			if (column.getIsId())
				IdColumns.add(column);
		});
	}

	private void createColumns(){
		Column column;
		Id id;

		String fieldName;
		ColumnInfo columnInfo;
		ConditionColumn filter;

		//处理属性上有Column注解的属性
		for (Field field:fields){
			if (field.getAnnotation(Transient.class) != null)
				continue;
			fieldName=field.getName();
			column=field.getAnnotation(Column.class);
			if (column != null){
				fieldName=field.getName();
				columnInfo=new ColumnInfo(fieldName, column.table(), column.name(),  field.getType(),java.sql.Types.NULL);
				filter=field.getAnnotation(ConditionColumn.class);
				if (filter!=null){
					columnInfo.setFilterable(filter.filterable());
					if (filter.filterable() && filter.filterName()!=null && filter.filterName().length()>0){
						columnInfo.setFilterName(filter.filterName());
					}
				}
			}else{
				columnInfo=getFieldColumn(fieldName, field);				
			}

			if (columnInfo != null ){
				id=field.getAnnotation(Id.class);
				columnInfo.setIsId(id != null);
				columns.put(fieldName, columnInfo);
			}
		}
		//方法上注解,没有标准对应的属性。
		for(Method method:methods){
			columnInfo=getColumnFromMethod(method);
			if (columnInfo!=null ){
				id=method.getAnnotation(Id.class);
				boolean isId=(id != null);
				columnInfo.setIsId(isId);
				if (!columns.containsKey(columnInfo.getFieldName())){
					columns.put(columnInfo.getFieldName(),columnInfo);
				}
				//根据方法或属性上标志位Id,设置位主键字段
				columnInfo=columns.get(columnInfo.getFieldName());
				isId=isId||columnInfo.getIsId();
				columnInfo.setIsId(isId);				
			}
		}
	}
	
	//处理属性对应的方法上有Column注解的方法
	private ColumnInfo getFieldColumn(String fieldName,Field field){
		Column column;
		String methodName;
		String fieldGetName;
		ColumnInfo columnInfo;

		for(Method method:methods){
			fieldGetName=methodName=method.getName();
			if (methodName.startsWith("get")){
				fieldGetName=methodName.substring(3);
			}else if (methodName.startsWith("is")){
				fieldGetName=methodName.substring(2);
			}
			if (fieldGetName.length()==0){
				System.out.println("methodName:"+methodName);
				continue;
			}
			fieldGetName=fieldGetName.substring(0,1).toLowerCase()+fieldGetName.substring(1);
			if (!fieldGetName.equals(fieldName) ){
				continue;
			}
			if (method.getAnnotation(Transient.class) != null)
				continue;
			column=method.getAnnotation(Column.class);
			if (column != null){
				columnInfo=new ColumnInfo(fieldName, column.table(), column.name(),  field.getType(),java.sql.Types.NULL);
			}else{
				columnInfo=new ColumnInfo(fieldName, "", fieldName,  field.getType(),java.sql.Types.NULL);				
			}
			ConditionColumn filter=field.getAnnotation(ConditionColumn.class);
			if (filter!=null) {
				columnInfo.setFilterable(filter.filterable());
				if (filter.filterable() && filter.filterName()!=null && filter.filterName().length()>0){
					columnInfo.setFilterName(filter.filterName());
				}
			}
			Id id=field.getAnnotation(Id.class);
			columnInfo.setIsId(id != null);
			return columnInfo;

		}
		return null;		
	}
	
	private ColumnInfo getColumnFromMethod(Method method){
		String methodName;
		String fieldGetName;
		ColumnInfo columnInfo;
		fieldGetName=methodName=method.getName();
		if (methodName.startsWith("get")){
			fieldGetName=methodName.substring(3);
		}else if (methodName.startsWith("is")){
			fieldGetName=methodName.substring(2);
		}
		fieldGetName=fieldGetName.substring(0,1).toLowerCase()+fieldGetName.substring(1);
		
		String fieldName=fieldGetName;
		Column column = method.getAnnotation(Column.class);
		Class<?> type=method.getReturnType();
		if (column != null){
			columnInfo=new ColumnInfo(fieldName, column.table(), column.name(),  type,java.sql.Types.NULL);
			ConditionColumn filter=method.getAnnotation(ConditionColumn.class);
			if (filter!=null){
				columnInfo.setFilterName(filter.filterName());
			}
			return columnInfo;
		}else{
			return null;
		}
		
	}
	
	private void createSelectSQL(){
	
		if (dataObjectType==BeanMappingType.Select){
			checkSQL();
			return;
		}
		StringBuffer selectFrom=new StringBuffer("select ");
		columns.forEach((field,column)->selectFrom.append(column.getColumnName()).append(","));
		selectFrom.replace(selectFrom.length()-1, selectFrom.length(), " ");
		selectSQL =selectFrom.append("\r\n from ").append(tableName).toString();
	} 
	private void checkSQL(){
		int[] indexes=SqlUtil.indexAllOfKeywordInSql(selectSQL, "select");
		if (indexes.length!=1){
			throw new RuntimeException("暂时不支持Union功能 ,selectSQL只能包含了1个select,selectSQL:"+selectSQL);
		}
		indexes=SqlUtil.indexAllOfKeywordInSql(selectSQL, "from");
		if (indexes.length!=1){
			throw new RuntimeException("暂时不支持Union功能 ,selectSQL只能包含了1个from,selectSQL:"+selectSQL);
		}
	}

	
	private void createCountSQL(){		
		if (dataObjectType==BeanMappingType.Select){
			checkSQL();
			return;
		}
		countSQL ="select count(*) \r\n from "+tableName;
	}

	private void createInsertSQL(){
		if (dataObjectType==BeanMappingType.Select){
			return ;
		}
		StringBuffer fields=new StringBuffer();
		StringBuffer values=new StringBuffer();
		Iterator<Entry<String,ColumnInfo>> colInfos=columns.entrySet().iterator();
		ColumnInfo column;
		for (int i=0;colInfos.hasNext();i++){
			column=colInfos.next().getValue();
			String columnName=column.getColumnName();
			if (columnName == null || columnName.length() == 0)
				continue;
			if (i>0){
				fields.append(",");
				values.append(",");
			}
			fields.append(column.getColumnName());
			values.append("#{entityBean.").append(column.getFieldName()).append("}");
		}

		insertSQL="insert into "+tableName+" ("+fields+") values ("+values+")";
	}
	
	private void createExistSQL(){
		StringBuffer existSql=new StringBuffer();
		existSql.append(" select count(*) from ").append(tableName).append(" where ");		
		int i=0;
		for(ColumnInfo col:IdColumns){
			if (i>0)
				existSql.append(" and ");
			existSql.append(col.getColumnName()).append(" = #{entityBean.").append(col.getFieldName()).append("}");	
			i++;
		}
		this.existSQL=existSql.toString();
	}
		
	private void createDeleteSQL(){
		if (dataObjectType==BeanMappingType.Select){
			return ;
		}
		StringBuffer dltSql=new StringBuffer();
		dltSql.append("delete from ").append(tableName).append(" where ");
		
		int i=0;
		for(ColumnInfo col:IdColumns){
			if (i>0)
				dltSql.append(" and ");
			dltSql.append(col.getColumnName()).append(" = #{entityBean.").append(col.getFieldName()).append("}");	
			i++;
		}
		this.deleteSQL=dltSql.toString();
	}

	private void createUpdateSQL(){
		if (dataObjectType==BeanMappingType.Select){
			return ;
		}
		StringBuffer updtSql=new StringBuffer();
		updtSql.append("update ").append(tableName).append(" set ");
		
		int i=0;
		String columnName;
		ColumnInfo column;
		for (Entry<String,ColumnInfo> colEntry:columns.entrySet()){
			column=colEntry.getValue();
			columnName=column.getColumnName();
			if (columnName == null || columnName.length() == 0)
				continue;
			if (column.getIsId())
				continue;
			if (i>0)
				updtSql.append(",");
			updtSql.append(column.getColumnName()).append(" = #{entityBean.").append(column.getFieldName()).append("}");
			i++;
		}
		i=0;
		updtSql.append(" where ");
		for(ColumnInfo col:IdColumns){
			if (i>0)
				updtSql.append(" and ");
			updtSql.append(col.getColumnName()).append(" = #{entityBean.").append(col.getFieldName()).append("}");	
			i++;
		}
		this.updateSQL=updtSql.toString();
	}

	private void createSaveSQL(){
		if (dataObjectType==BeanMappingType.Select){
			return ;
		}
		StringBuffer fields=new StringBuffer();
		StringBuffer values=new StringBuffer();
		Iterator<Entry<String,ColumnInfo>> colInfos=columns.entrySet().iterator();
		ColumnInfo column;
		for (int i=0;colInfos.hasNext();i++){
			column=colInfos.next().getValue();
			String columnName=column.getColumnName();
			if (columnName == null || columnName.length() == 0)
				continue;
			if (i>0){
				fields.append(",");
				values.append(",");
			}
			fields.append(column.getColumnName());
			values.append("#{entityBean.").append(column.getFieldName()).append("}");
		}
		StringBuffer savesql=new StringBuffer();
		savesql.append("insert into ").append(tableName)
				.append(" (").append(fields)
				.append(") values (").append(values).append(") ON DUPLICATE KEY UPDATE ");

		int i=0;
		String columnName;
		for (Entry<String,ColumnInfo> colEntry:columns.entrySet()){
			column=colEntry.getValue();
			columnName=column.getColumnName();
			if (columnName == null || columnName.length() == 0)
				continue;
			if (column.getIsId())
				continue;
			if (i>0)
				savesql.append(",");
			savesql.append(column.getColumnName()).append(" = #{entityBean.").append(column.getFieldName()).append("}");
			i++;
		}
	
		this.saveSQL=savesql.toString();
	}

	private void createGetByIdSQL(){
		if (dataObjectType==BeanMappingType.Select){
			return ;
		}
		StringBuffer getSql=new StringBuffer();
		getSql.append("select * from ").append(tableName).append(" where ");
		
		int i=0;
		for(ColumnInfo col:IdColumns){
			if (i>0)
				getSql.append(" and ");
			getSql.append(col.getColumnName()).append(" = #{entityId.").append(col.getFieldName()).append("}");	
			i++;
		}
		this.getByIdSQL=getSql.toString();
	}
	
	public String getSelectFrom(){
		return selectSQL;
	}
	
	
	public String getTableName() {
		return tableName;
	}

	public void setTableName(String tableName) {
		this.tableName = tableName;
	}

	public String getSelectSQL() {
		return selectSQL;
	}

	public void setSelectSQL(String selectString) {
		this.selectSQL = selectString;
	}
    
    ……(略去部分)
    
	@Override
	public Iterator<Entry<Class<?>, EntityBeanInfo>> iterator() {
		return entityBeanInfos.entrySet().iterator();	
	} 
}
2. ColumnInfo类
public class ColumnInfo {
	private String fieldName;
	
	private boolean isId=false;
	private String tableName;
	//结果集字段名
	private String columnName;
	//条件字段名
	private String filterName;
	private boolean filterable=true;
	private Class<?> javaType;
	private int jdbcType;
	
	public ColumnInfo(String fieldName, String tableName, String columnName, Class<?> javaType,int jdbcType){
		this.fieldName=fieldName;
		this.tableName=tableName;
		this.columnName=columnName;
		this.javaType=javaType;
		this.jdbcType=jdbcType;
		if (this.columnName==null || this.columnName.trim().length()==0){
			this.columnName=this.fieldName;
		}
		filterName=this.columnName;
	}
	
	public String getFieldName() {
		return fieldName;
	}
	public void setFieldName(String fieldName) {
		this.fieldName = fieldName;
	}
	public boolean getIsId() {
		return isId;
	}

	public void setIsId(boolean isId) {
		this.isId = isId;
	}

	public String getTableName() {
		return tableName;
	}
	public void setTableName(String tableName) {
		this.tableName = tableName;
	}
    
……(略去部分)

}
D. 扩展ExecutorInterceptor。

对于分页查询,需要提供数据库端分页,同时获取满足条件的数据行数,需要分两次执行,第一次执行select count(*) from ……,获得记录数后,再执行select …… from ……获取查询记录,因此定义一个PagedExecutorInterceptor,截获掉框架的Executor,分两次调用Executor的query方法,并组装成分页数据,返回给调用者。

PagedExecutorInterceptor类代码:

@Service
@Intercepts({ @Signature(type = Executor.class, method = "query", args = {  
        MappedStatement.class, Object.class, RowBounds.class,  
        ResultHandler.class }) })  
public class PagedExecutorInterceptor implements Interceptor {
	
	private final Logger logger = LoggerFactory.getLogger(this.getClass());
	
    static int MAPPED_STATEMENT_INDEX = 0;  
    static int PARAMETER_INDEX = 1;  
    static int ROWBOUNDS_INDEX = 2;  
    static int RESULT_HANDLER_INDEX = 3;  
  
    IDialect dialect=new MySQLDialect();  
    
    private Properties properties;
  
    public Object intercept(Invocation invocation) throws Throwable {  
    	Object[] queryArgs = invocation.getArgs();
        Object parameter = queryArgs[PARAMETER_INDEX];  
        Paginate<?> paginate=getPaginate((HashMap<String,Object>)parameter);
        
        if (paginate != null && parameter instanceof HashMap ){
        	return execute(invocation,queryArgs,paginate);    
        }
        return invocation.proceed();     
        
    }  
  
    protected Object execute(Invocation invocation,Object[] queryArgs,Paginate<?> paginate
    		) throws Throwable{

        MappedStatement ms = (MappedStatement) queryArgs[MAPPED_STATEMENT_INDEX];
        List<ResultMap> resultMaps=ms.getResultMaps();
        int paginateIndex=getIndexInResultMap(resultMaps,Paginate.class);
        Object parameter = queryArgs[PARAMETER_INDEX];  
        BoundSql boundSql = ms.getBoundSql(parameter);  
        String sql = boundSql.getSql().trim();  
        
        
        HashMap<String,Object>  params=(HashMap<String,Object>) parameter;
        Class<?> beanClass=(Class<?>)params.get("entityClass");
        
		// 获取统计行数
        String cntSql=getRowCountSQL(sql);
		processIntercept(boundSql,queryArgs, paginate, Integer.class, paginateIndex, cntSql);
		Object rowCount = invocation.proceed();
		if (rowCount instanceof List) {
			List<Integer> rows = (List<Integer>) rowCount;
			int totalRows = rows.get(0);
			paginate.setTotalRows(totalRows);
			int totalPages = (totalRows + paginate.getPageSize() - 1) / paginate.getPageSize();
			paginate.setTotalPages(totalPages);
		}
        //获取分页数据
        String pagedSql=getPagedSQL(sql,paginate);
    	processIntercept(boundSql,queryArgs,paginate,beanClass,paginateIndex,pagedSql);  
        Object rows=invocation.proceed();
        paginate.setDatas((List)rows);
        
        List result=new ArrayList();
        result.add(paginate);
        
    	return result;
    }
    
    /**
     * 获取生成行数sql,本方法只是简单找到FROM位置和order by 位置。
     * 暂时不支持对 包含union 字句的功能,支持包含有子查询的sql
     * @param sql
     * @return
     */
    protected String getRowCountSQL(String sql){
    	int selectPos=SqlUtil.indexOfKeywordInSql(sql, "select");
    	int[] fromPoses=SqlUtil.indexAllOfKeywordInSql(sql, "from");
    	String lowsql=sql.toLowerCase();
    	String cntSql="select count(*) from ";
    	if (fromPoses.length==1){
        	int orderPos = lowsql.indexOf("order ",fromPoses[0]);
        	if (orderPos>0)
    			cntSql=sql.substring(0,selectPos)+cntSql+sql.substring(fromPoses[0]+5,orderPos);
    		else
    			cntSql=sql.substring(0,selectPos)+cntSql+sql.substring(fromPoses[0]+5);
    	}else{
    		cntSql=cntSql+"("+sql+") as t";
    	}
        logger.debug("mybatis get rowCount sql:{}", cntSql);
    	return cntSql;
    }
    protected String getPagedSQL(String sql,Paginate<?> paginate){
        String slctSql=sql;
		if (paginate.getPageNo() < 1) {
			paginate.setPageNo(1);
		}
		int offset = (paginate.getPageNo() - 1) * paginate.getPageSize();
		int limit = paginate.getPageSize();
		
        if (dialect.supportsLimitOffset()) {  
        	slctSql = dialect.getLimitString(slctSql, offset, limit);  
        } else {  
        	slctSql = dialect.getLimitString(slctSql, 0, limit);  
        }
        logger.debug("mybatis pagedQuery sql:{}", slctSql);
        return slctSql;
    }
    protected void processIntercept(BoundSql boundSql,final Object[] queryArgs,Paginate<?> paginate,Class<?> type,int index,String sql) {  

    	MappedStatement ms = (MappedStatement) queryArgs[MAPPED_STATEMENT_INDEX];  
        final RowBounds rowBounds = (RowBounds) queryArgs[ROWBOUNDS_INDEX];  
        
        if (dialect.supportsLimit() ) {  
            MappedStatement newMs = createMappedStatement(type,index,ms,sql,boundSql);  
            queryArgs[MAPPED_STATEMENT_INDEX] = newMs;                          
            queryArgs[ROWBOUNDS_INDEX]=new PagedRowBounds(type,paginate, rowBounds);
        }
    }  
    
    private Paginate<?> getPaginate(HashMap<String,Object> params){
		for (Map.Entry<String, Object> param : params.entrySet()) {
			if (param.getValue() instanceof Paginate)
				return (Paginate) param.getValue();
		}    		
    	return null;
    }
    
    private int getIndexInResultMap(List<ResultMap> resultMaps,Class<?> type){
        ResultMap rm;
        Class<?> entityClass;
        for(int i=0;i<resultMaps.size();i++){
        	rm=resultMaps.get(i);
        	entityClass=rm.getType();
        	if (entityClass.equals(type)){
        		return i;
        	}        	
        }
        return -1;
   	
    }
    
    private MappedStatement createMappedStatement(Class<?> beanClass,int index,MappedStatement ms,String sql,BoundSql boundSql){
       	
        BoundSql newBoundSql = new PagedBoundSql(beanClass,ms.getConfiguration(), sql,  
                boundSql.getParameterMappings(),  
                boundSql.getParameterObject());  
        MappedStatement newMs =InterceptorUtil.copyFromMappedStatement(beanClass,index,ms,   new BoundSqlSqlSource(newBoundSql));  
    	return newMs;
    }
    
    public Object plugin(Object target) {  
        return Plugin.wrap(target, this);  
    }  
  
    public void setProperties(Properties properties) {  
    	this.properties = properties; 
    }  
  
}

如何使用

只需要写好EntityBean,Mapper只需要扩展GenericMapper,就可以调用基类接口中的方法了。
1.定义个EntityBean或者定义一个查询Bean

先来个实体bean

@Entity
@Table(name = "AppInfo")
public class AppInfo {
	@Id
	private String appId;
	private String name;
	……(部分省略)
	public String getAppId() {
		return appId;
	}
	public void setAppId(String appId) {
		this.appId = appId;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
    ……(部分省略)
}

再来个查询bean

@Entity
@BeanSQLMap(mappingSQL = "select d.*,c.name as confName,c.value as configValue "
		+ "from RfcServiceDef d join RfcServiceConfig c on d.id=c.serviceId ",
		countSQL = "select count(*) "
				+ "from RfcServiceDef d join RfcServiceConfig c on d.id=c.serviceId ")
public class RfcServiceAndConfig {
	private int id;
	private String name;

	……(部分省略)

	@Column(name="confName")
	@ConditionColumn(filterName="c.name")
	private String confName;
	@Column(name="configValue")
	@ConditionColumn(filterName="c.value")
	private String configValue;
	
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getConfName() {
		return confName;
	}
	public void setConfName(String confName) {
		this.confName = confName;
	}
	public String getConfigValue() {
		return configValue;
	}
	public void setConfigValue(String configValue) {
		this.configValue = configValue;
	}
}

2.继承扩展一下GenericMapper

@Mapper
public interface AppInfoMapper extends GenericMapper<AppInfo> {
}
@Mapper
public interface RfcServiceAndConfigMapper extends GenericMapper<RfcServiceAndConfig> {
	
}

现在,你不用写基类方法,也不用配XML,就可以使用了,下面来个例子。

	@Autowired
    private AppInfoMapper appInfoMapper;
	
@Autowired
	RfcServiceAndConfigMapper rfcServiceAndConfigMapper;
    
……(部分省略)

Map<String,Object> paras=new HashMap<>(); 
paras.put("appId_like","2019");
paras.put("name","零售抽奖");
List<AppInfo> result =appInfoMapper.query(AppInfo.class, paras);
……(部分省略)
Map<String,Object> paras=new HashMap<>(); 
paras.put("chnName_like", "会员");
Paginate<RfcServiceAndConfig> paginate=new Paginate<>();
paginate.setPageNo(2);
paginate.setPageSize(10);
Paginate<RfcServiceAndConfig> result = rfcServiceAndConfigMapper.pagedQuery(RfcServiceAndConfig.class,paginate, paras,"");

写在后面

通过粗浅的学习了解,个人觉得:
1.Hibernate学习的成本比Mybatis大,特别是他基于对象的视角看待数据,表与表数据关系体现为实体数据间关系,数据操作主要采用Hql,要掌握Hql,需要开发人员从对象及对象关系的视角重新审视SQL,对开发人员OOP的能力要求稍微更高一些。
2.Mybatis主要以手动编写SQL语句以及ResultMap的方式使用,重点关注在数据上。而Hibernate映射机制封装度的抽象层级更高,开发人员不需要关心SQL的生成与结果映射,让开发人员更专注于业务流程和数据关系上。
3. 相比Hibernate来看,Mybatis还是要简陋一些,在持久化层要低一些。Mybatis宣称让开发人员可以自由写SQL,成为他流行的两点,降低开发人员门槛,也就降低了开发的成本。当然使用Hibernate,如果遇到性能问题时一样可以直接使用SQL,这在以前的Hibernate项目中依然将SQL作为性能调优的强有力的手段。
4.Hibernate的HQL语句与具体使用的数据库无关,移植性很好。MyBatis项目中所有的SQL语句都是依赖所用的数据库的,所以数据库移植的支持度相对差。

这只是个人的一些使用体会,不是为了评价或测评这两个框架的优缺点,当然也不是为了挑起两大阵营间的论战,这不是我的初衷。
再有就是,由于使用还没有三周,一定会有很多地方理解错误,或者使用上的错误,还望不吝赐教,批评指正。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值