xin.xihc.jba.core.bean.JbaBeanPropertyRowMapper Maven / Gradle / Ivy
package xin.xihc.jba.core.bean;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.*;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import xin.xihc.jba.annotation.Alias;
import xin.xihc.jba.annotation.Column;
import xin.xihc.utils.common.CommonUtil;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.*;
/**
* 自定义行映射关系
*
* @author Leo.Xi
* @date 2019/4/30 17:40
* @version 1.0
**/
public class JbaBeanPropertyRowMapper implements RowMapper {
protected static final LinkedHashMap RowMapper_CACHE = new LinkedHashMap<>();
/**
* Logger available to subclasses
*/
protected final Log logger = LogFactory.getLog(getClass());
/**
* The class we are mapping to
*/
private Class mappedClass;
/**
* Whether we're strictly validating
*/
private boolean checkFullyPopulated = false;
/**
* Whether we're defaulting primitives when mapping a null value
*/
private boolean primitivesDefaultedForNullValue = false;
/**
* ConversionService for binding JDBC values to bean properties
*/
private ConversionService conversionService = DefaultConversionService.getSharedInstance();
/**
* Map of the fields we provide mapping for
*/
private Map mappedFields;
/**
* Set of bean properties we provide mapping for
*/
private Set mappedProperties;
/**
* Create a new {@code BeanPropertyRowMapper}, accepting unpopulated
* properties in the target bean.
* Consider using the {@link #newInstance} factory method instead,
* which allows for specifying the mapped type once only.
*
* @param mappedClass the class that each row should be mapped to
*/
public JbaBeanPropertyRowMapper(Class mappedClass) {
initialize(mappedClass);
}
/**
* Create a new {@code BeanPropertyRowMapper}.
*
* @param mappedClass the class that each row should be mapped to
* @param checkFullyPopulated whether we're strictly validating that
* all bean properties have been mapped from corresponding database fields
*/
public JbaBeanPropertyRowMapper(Class mappedClass, boolean checkFullyPopulated) {
initialize(mappedClass);
this.checkFullyPopulated = checkFullyPopulated;
}
/**
* Static factory method to create a new {@code BeanPropertyRowMapper}
* (with the mapped class specified only once).
*
* @param mappedClass the class that each row should be mapped to
*/
public static org.springframework.jdbc.core.BeanPropertyRowMapper newInstance(Class mappedClass) {
return new org.springframework.jdbc.core.BeanPropertyRowMapper(mappedClass);
}
/**
* Get the class that we are mapping to.
*/
public final Class getMappedClass() {
return this.mappedClass;
}
/**
* Set the class that each row should be mapped to.
*/
public void setMappedClass(Class mappedClass) {
if (this.mappedClass == null) {
initialize(mappedClass);
} else {
if (this.mappedClass != mappedClass) {
throw new InvalidDataAccessApiUsageException(
"The mapped class can not be reassigned to map to " + mappedClass + " since it is already providing mapping for " + this.mappedClass);
}
}
}
/**
* Return whether we're strictly validating that all bean properties have been
* mapped from corresponding database fields.
*/
public boolean isCheckFullyPopulated() {
return this.checkFullyPopulated;
}
/**
* Set whether we're strictly validating that all bean properties have been mapped
* from corresponding database fields.
* Default is {@code false}, accepting unpopulated properties in the target bean.
*/
public void setCheckFullyPopulated(boolean checkFullyPopulated) {
this.checkFullyPopulated = checkFullyPopulated;
}
/**
* Return whether we're defaulting Java primitives in the case of mapping a null value
* from corresponding database fields.
*/
public boolean isPrimitivesDefaultedForNullValue() {
return this.primitivesDefaultedForNullValue;
}
/**
* Set whether we're defaulting Java primitives in the case of mapping a null value
* from corresponding database fields.
*
Default is {@code false}, throwing an exception when nulls are mapped to Java primitives.
*/
public void setPrimitivesDefaultedForNullValue(boolean primitivesDefaultedForNullValue) {
this.primitivesDefaultedForNullValue = primitivesDefaultedForNullValue;
}
/**
* Return a {@link ConversionService} for binding JDBC values to bean properties,
* or {@code null} if none.
*
* @since 4.3
*/
public ConversionService getConversionService() {
return this.conversionService;
}
/**
* Set a {@link ConversionService} for binding JDBC values to bean properties,
* or {@code null} for none.
*
Default is a {@link DefaultConversionService}, as of Spring 4.3. This
* provides support for {@code java.time} conversion and other special types.
*
* @see #initBeanWrapper(BeanWrapper)
* @since 4.3
*/
public void setConversionService(ConversionService conversionService) {
this.conversionService = conversionService;
}
/**
* Initialize the mapping meta-data for the given class.
*
* @param mappedClass the mapped class
*/
protected void initialize(Class mappedClass) {
this.mappedClass = mappedClass;
if (!RowMapper_CACHE.containsKey(mappedClass)) {
this.mappedFields = new LinkedHashMap<>(16);
this.mappedProperties = new HashSet<>();
// 所有属性
List allFields = CommonUtil.getAllFields(mappedClass, false, false);
PropertyDescriptor[] pds = BeanUtils.getPropertyDescriptors(mappedClass);
for (PropertyDescriptor pd : pds) {
if (pd.getWriteMethod() != null) {
this.mappedFields.put(lowerCaseName(pd.getName()), pd);
String underscoredName = underscoreName(pd.getName());
if (!lowerCaseName(pd.getName()).equals(underscoredName)) {
this.mappedFields.put(underscoredName, pd);
}
// 增加字段映射关系
Optional field = allFields.stream().filter(x -> pd.getName().equals(x.getName()))
.findFirst();
field.ifPresent(f -> {
// @Column
Column fColumn = f.getAnnotation(Column.class);
if (null != fColumn && CommonUtil.isNotNullEmpty(fColumn.value())) {
this.mappedFields.put(fColumn.value(), pd);
}
// @Alias
Alias fAlias = f.getAnnotation(Alias.class);
if (null != fAlias) {
for (String as : fAlias.value()) {
this.mappedFields.put(as, pd);
}
}
});
this.mappedProperties.add(pd.getName());
}
}
RowMapper_CACHE.put(mappedClass, this);
} else {
this.mappedFields = RowMapper_CACHE.get(mappedClass).mappedFields;
this.mappedProperties = RowMapper_CACHE.get(mappedClass).mappedProperties;
}
}
/**
* Convert a name in camelCase to an underscored name in lower case.
* Any upper case letters are converted to lower case with a preceding underscore.
*
* @param name the original name
* @return the converted name
* @see #lowerCaseName
* @since 4.2
*/
protected String underscoreName(String name) {
if (!StringUtils.hasLength(name)) {
return "";
}
StringBuilder result = new StringBuilder();
result.append(lowerCaseName(name.substring(0, 1)));
for (int i = 1; i < name.length(); i++) {
String s = name.substring(i, i + 1);
String slc = lowerCaseName(s);
if (!s.equals(slc)) {
result.append("_").append(slc);
} else {
result.append(s);
}
}
return result.toString();
}
/**
* Convert the given name to lower case.
* By default, conversions will happen within the US locale.
*
* @param name the original name
* @return the converted name
* @since 4.2
*/
protected String lowerCaseName(String name) {
return name.toLowerCase(Locale.US);
}
/**
* Extract the values for all columns in the current row.
* Utilizes public setters and result set meta-data.
*
* @see java.sql.ResultSetMetaData
*/
@Override
public T mapRow(ResultSet rs, int rowNumber) throws SQLException {
Assert.state(this.mappedClass != null, "Mapped class was not specified");
T mappedObject = BeanUtils.instantiateClass(this.mappedClass);
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(mappedObject);
initBeanWrapper(bw);
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();
Set populatedProperties = (isCheckFullyPopulated() ? new HashSet() : null);
for (int index = 1; index <= columnCount; index++) {
String column = JdbcUtils.lookupColumnName(rsmd, index);
String field = lowerCaseName(column.replaceAll(" ", ""));
PropertyDescriptor pd = this.mappedFields.get(field);
if (pd != null) {
try {
Object value = getColumnValue(rs, index, pd);
if (rowNumber == 0 && logger.isDebugEnabled()) {
logger.debug("Mapping column '" + column + "' to property '" + pd
.getName() + "' of type '" + ClassUtils.getQualifiedName(pd.getPropertyType()) + "'");
}
try {
bw.setPropertyValue(pd.getName(), value);
} catch (TypeMismatchException ex) {
if (value == null && this.primitivesDefaultedForNullValue) {
if (logger.isDebugEnabled()) {
logger.debug(
"Intercepted TypeMismatchException for row " + rowNumber + " and column '" + column + "' with null value when setting property '" + pd
.getName() + "' of type '" + ClassUtils.getQualifiedName(
pd.getPropertyType()) + "' on object: " + mappedObject, ex);
}
} else {
throw ex;
}
}
if (populatedProperties != null) {
populatedProperties.add(pd.getName());
}
} catch (NotWritablePropertyException ex) {
throw new DataRetrievalFailureException(
"Unable to map column '" + column + "' to property '" + pd.getName() + "'", ex);
}
} else {
// No PropertyDescriptor found
if (rowNumber == 0 && logger.isDebugEnabled()) {
logger.debug("No property found for column '" + column + "' mapped to field '" + field + "'");
}
}
}
if (populatedProperties != null && !populatedProperties.equals(this.mappedProperties)) {
throw new InvalidDataAccessApiUsageException(
"Given ResultSet does not contain all fields " + "necessary to populate object of class [" + this.mappedClass
.getName() + "]: " + this.mappedProperties);
}
return mappedObject;
}
/**
* Initialize the given BeanWrapper to be used for row mapping.
* To be called for each row.
* The default implementation applies the configured {@link ConversionService},
* if any. Can be overridden in subclasses.
*
* @param bw the BeanWrapper to initialize
* @see #getConversionService()
* @see BeanWrapper#setConversionService
*/
protected void initBeanWrapper(BeanWrapper bw) {
ConversionService cs = getConversionService();
if (cs != null) {
bw.setConversionService(cs);
}
}
/**
* Retrieve a JDBC object value for the specified column.
*
The default implementation calls
* {@link JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class)}.
* Subclasses may override this to check specific value types upfront,
* or to post-process values return from {@code getResultSetValue}.
*
* @param rs is the ResultSet holding the data
* @param index is the column index
* @param pd the bean property that each result object is expected to match
* @return the Object value
* @throws SQLException in case of extraction failure
* @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class)
*/
protected Object getColumnValue(ResultSet rs, int index, PropertyDescriptor pd) throws SQLException {
return JdbcUtils.getResultSetValue(rs, index, pd.getPropertyType());
}
}