詳解如何開發(fā)一個MyBatis通用Mapper的輪子
一、前言
程序猿為什么如此執(zhí)著于造輪子?MyBatis-Plus如此強大的工具流行這么多年了,我為啥還在重復造這樣的輪子?
1、公司的技術規(guī)范不允許使用MyBatis-Plus,咱也不知道什么原因;
3、以前使用SpringDataJpa慣了,今年第一次用MyBatis,必須把它打造成我想要的樣子;
6、MyBatis-Plus好像不支持聯(lián)合主鍵;
7、還有一些其它的需求,比如對字典字段自動翻譯:字典可能來自枚舉、字典表、Redis......
10、通用數(shù)據(jù)權限控制;
11、如果不造此輪子,就沒有這篇文章。
以上12點原因,便是造這個輪子的理由。實際上,輪子不重要,重要的是掌握輪子的原理,取其精華,去其糟粕。也歡迎大家拍磚,請輕拍,數(shù)學能力被誰拍壞了誰來陪。
二、需求
通用Mapper起碼應該包含以下功能:
1、增
2、刪
3、改
4、批量增
5、批量刪
6、只更新指定字段
7、分頁查詢查當前頁
8、分頁查詢查總數(shù)
9、字典字段翻譯
10、數(shù)據(jù)權限控制
大概長下面這個樣子:
public interface BaseMapper<T,K> {
int insert(T t);
int batchInsert(List<T> entity);
int deleteById(K id);
int deleteBatchIds(Collection<K> ids);
int updateById(T entity);
int updateSelectiveById(T entity);
T selectById(K id);
List<T> selectBatchIds(Collection<K> ids);
List<T> selectAll();
List<T> selectPage(PageRequest<T> pageRequest);
Long selectCount(T entity);
}三、實現(xiàn)原理
1、基于MyBatis3提供的SqlProvider構建動態(tài)Sql

例如如下代碼:
@SelectProvider(type = UserSqlBuilder.class, method = "buildGetUsersByName")
List<User> getUsersByName(String name);
class UserSqlBuilder {
public static String buildGetUsersByName(final String name) {
return new SQL(){{
SELECT("*");
FROM("users");
if (name != null) {
WHERE("name like #{value} || '%'");
}
ORDER_BY("id");
}}.toString();
}
}2、基于自定義注解,為實體和數(shù)據(jù)庫表建立對應關系
例如如下代碼:
@Table("user")
public class User {
@Id(auto = true)
@Column(value = "id")
private Long id;
@Column(value = "name", filterOperator = FilterOperator.LIKE)
@OrderBy(orderPriority = 0)
private String name;
@OrderBy(order = Order.DESC, orderPriority = 1)
private Integer age;
private String email;
@Transient
private String test;
}基于以上兩個原理,當方法被調(diào)用時,我們便可構建出相應的動態(tài)Sql,從而實現(xiàn)該通用Mapper。
四、代碼實現(xiàn)
1、自定義注解
1)@Table
了解Jpa的朋友一定很熟悉,這個就是為實體指定表名,實體不加這個注解就認為實體名與表名一致:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Table {
//表名,不指定則使用實體類名
String value() default "";
}2)@Column
指定完表名,該指定列名了,同樣的如果字段不指定則認為字段名與表列名一致:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Column {
//對應數(shù)據(jù)庫列名
String value() default "";
//查詢時的過濾類型
FilterOperator filterOperator() default FilterOperator.EQ;
//是否查詢,select是否帶上該字段
boolean selectable() default true;
//是否插入,insert是否帶上該字段
boolean insertable() default true;
//是否更新,update是否帶上該字段
boolean updatable() default true;
}3)@Id
這個注解就是為了表明該字段是否是數(shù)據(jù)庫主鍵。當然,這個注解可以與@Column合并,但為了更清晰,我還是決定單獨使用這個注解。并且,也方便后期擴展。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Id {
//主鍵是否自動生成
boolean auto() default false;
}4)@OrderBy
這個注解來標明查詢時的排序字段,同時考慮如果排序字段有多個,可定義優(yōu)先級:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface OrderBy {
//排序
Order order() default Order.ASC;
//多個排序字段先后順序
int orderPriority() default 0;
}5)@Transient
考慮實體中有些字段在數(shù)據(jù)庫中不存在的情況。使用這個注解來標注這樣的字段:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Transient {
}2、幾個pojo,用來保存實體對應的信息
1)TableInfo,表示一個實體對應的數(shù)據(jù)庫表信息
public class TableInfo {
//表對應的實體類型
private Class<?> entityClass;
//表名
private String tableName;
//列
private List<ColumnInfo> columns;
//是否聯(lián)合主鍵
private boolean isUnionId;
}2)ColumnInfo,表示實體中的一個字段對應的數(shù)據(jù)庫表字段信息
public class ColumnInfo {
//對應的java類型
private Class<?> fieldClass;
private Field field;
private FilterOperator filterOperator;
//數(shù)據(jù)庫列
private String column;
//是否主鍵
private boolean isPrimaryKey;
//主鍵填充方式
private boolean isPrimaryKeyAuto;
//排序
private Order orderBy;
private int orderByPriority;
//是否參與insert
private boolean insertable;
//是否參與update
private boolean updatable;
//是否參與select
private boolean selectable;
}以上只需要注意一點,如何判斷一個實體是否是聯(lián)合主鍵。這里用的比較粗暴的方法,如果有多個字段加了@Id,那么認為是聯(lián)合主鍵。
3、定義開頭說的BaseMapper
這個BaseMapper的定義模仿了SpringDataJpa,它需要兩個泛型,T表示實體類型,K表示主鍵類型。
一般情況下K為簡單數(shù)據(jù)類型,比如Long,String;
聯(lián)合主鍵情況下,K為自定義的一個復雜數(shù)據(jù)類型,具體使用方法見文章最后章節(jié)。
public interface BaseMapper<T,K> {
@InsertProvider(type = SqlProvider.class,method = "insert")
@Options(useGeneratedKeys = true, keyProperty = "id",keyColumn = "id")
int insert(T t);
@InsertProvider(type = SqlProvider.class,method = "batchInsert")
int batchInsert(@Param("list") List<T> entity);
@DeleteProvider(type = SqlProvider.class,method = "deleteById")
int deleteById(@Param("id") K id);
@DeleteProvider(type = SqlProvider.class,method = "deleteBatchIds")
int deleteBatchIds(@Param("ids") Collection<K> ids);
@UpdateProvider(type = SqlProvider.class,method = "updateById")
int updateById(T entity);
@UpdateProvider(type = SqlProvider.class,method = "updateSelectiveById")
int updateSelectiveById(T entity);
@SelectProvider(type = SqlProvider.class,method = "selectById")
T selectById(@Param("id") K id);
@SelectProvider(type = SqlProvider.class,method = "selectBatchIds")
List<T> selectBatchIds(@Param("ids") Collection<K> ids);
@SelectProvider(type = SqlProvider.class,method = "selectAll")
List<T> selectAll();
@SelectProvider(type = SqlProvider.class,method = "selectPage")
List<T> selectPage(PageRequest<T> pageRequest);
@SelectProvider(type = SqlProvider.class,method = "selectCount")
Long selectCount(T entity);
}4、SqlProvider
public class SqlProvider<T> {
private static Logger logger = LoggerFactory.getLogger(SqlProvider.class);
private static Map<Class<?>, TableInfo> tableCache = new ConcurrentHashMap<>();
public String insert(T entity, ProviderContext context){
TableInfo tableInfo = getTableInfo(context);
String tableName = tableInfo.getTableName();
String intoColumns = tableInfo.getColumns()
.stream()
.filter(ColumnInfo::isInsertable)
.map(ColumnInfo::getColumn)
.collect(Collectors.joining(","));
String values = tableInfo.getColumns()
.stream()
.filter(ColumnInfo::isInsertable)
.map(ColumnInfo::variable)
.collect(Collectors.joining(","));
String sql = new SQL()
.INSERT_INTO(tableName)
.INTO_COLUMNS(intoColumns)
.INTO_VALUES(values).toString();
logger.info("sql->{},params->{}",sql,entity);
return sql;
}
public String batchInsert(@Param("list" ) List<?> entity, ProviderContext context){
TableInfo tableInfo = getTableInfo(context);
String tableName = tableInfo.getTableName();
String intoColumns = tableInfo.getColumns()
.stream()
.filter(ColumnInfo::isInsertable)
.map(ColumnInfo::getColumn)
.collect(Collectors.joining(","));
String values = tableInfo.getColumns()
.stream()
.filter(ColumnInfo::isInsertable)
.map(column->column.variableWithPrefix("item"))
.collect(Collectors.joining(","));
String sql = new SQL()
.INSERT_INTO(tableName)
.INTO_COLUMNS(intoColumns).toString();
sql += " values ";
sql += "<foreach collection=\"list\" item=\"item\" separator=\",\">" +
" <trim prefix=\"(\" suffix=\")\" suffixOverrides=\",\">" +
" " + values +
" </trim>" +
"</foreach>";
sql = "<script>"+sql+"</script>";
logger.info("sql->{},params->{}",sql,entity);
return sql;
}
public String deleteById(@Param("id") T entity, ProviderContext context){
TableInfo tableInfo = getTableInfo(context);
String tableName = tableInfo.getTableName();
String[] where = null;
if (tableInfo.isUnionId()){
where = tableInfo.getColumns()
.stream()
.filter(ColumnInfo::isPrimaryKey)
.map(columnInfo -> columnInfo.getColumn()+" = #{id."+columnInfo.getField().getName()+"}")
.toArray(String[]::new);
}else {
where = tableInfo.getColumns()
.stream()
.filter(ColumnInfo::isPrimaryKey)
.map(columnInfo -> columnInfo.getColumn()+" = #{id}")
.toArray(String[]::new);
}
String sql = new SQL()
.DELETE_FROM(tableName)
.WHERE(where)
.toString();
logger.info("sql->{},params->{}",sql,entity);
return sql;
}
public String deleteBatchIds(@Param("ids") Collection<?> entity, ProviderContext context){
TableInfo tableInfo = getTableInfo(context);
String tableName = tableInfo.getTableName();
if (tableInfo.isUnionId()){
String[] where = new String[entity.size()];
for (int i = 0; i < entity.size(); i++){
List<String> list = new ArrayList<>();
String s = "%s=#{ids[%d].%s}";
for (ColumnInfo columnInfo:tableInfo.getColumns()){
if (columnInfo.isPrimaryKey()){
list.add(String.format(s,columnInfo.getColumn(),i,columnInfo.getField().getName()));
}
}
where[i] = "("+StringUtils.join(list," and ")+")";
}
String sql = "delete from %s where %s ";
sql = String.format(sql,tableName,StringUtils.join(where," or "));
logger.info("sql->{},params->{}",sql,entity);
return sql;
}else {
String idName = tableInfo.getColumns()
.stream()
.filter(ColumnInfo::isPrimaryKey)
.findFirst()
.get()
.getColumn();
String sql = "DELETE FROM %s WHERE %s IN (%s) ";
String[] arr = new String[entity.size()];
for (int i = 0; i < entity.size(); i++){
arr[i] = "#{ids["+i+"]}";
}
sql = String.format(sql,tableName,idName,StringUtils.join(arr,","));
logger.info("sql->{},params->{}",sql,entity);
return sql;
}
}
public String updateById(T entity, ProviderContext context){
TableInfo tableInfo = getTableInfo(context);
String tableName = tableInfo.getTableName();
String[] where = tableInfo.getColumns()
.stream()
.filter(ColumnInfo::isPrimaryKey)
.map(columnInfo -> columnInfo.getColumn()+" = "+columnInfo.variable())
.toArray(String[]::new);
String sql = new SQL().UPDATE(tableName).SET(tableInfo.updateSetColumn()).WHERE(where).toString();
logger.info("sql->{},params->{}",sql,entity);
return sql;
}
public String updateSelectiveById(T entity, ProviderContext context){
TableInfo tableInfo = getTableInfo(context);
String tableName = tableInfo.getTableName();
String[] where = tableInfo.getColumns()
.stream()
.filter(ColumnInfo::isPrimaryKey)
.map(columnInfo -> columnInfo.getColumn()+" = "+columnInfo.variable())
.toArray(String[]::new);
String sql = new SQL().UPDATE(tableName).SET(tableInfo.updateSetSelectiveColumn(entity)).WHERE(where).toString();
logger.info("sql->{},params->{}",sql,entity);
return sql;
}
public String selectById(@Param("id")T entity, ProviderContext context){
TableInfo tableInfo = getTableInfo(context);
String[] where = null;
if (tableInfo.isUnionId()){
where = tableInfo.getColumns().stream().filter(ColumnInfo::isPrimaryKey)
.map(columnInfo -> columnInfo.getColumn()+" = #{id."+columnInfo.getField().getName()+"}")
.toArray(String[]::new);
}else {
where = tableInfo.getColumns().stream().filter(ColumnInfo::isPrimaryKey)
.map(columnInfo -> columnInfo.getColumn()+" = #{id}")
.toArray(String[]::new);
}
String sql = new SQL()
.SELECT(tableInfo.selectColumnAsProperty())
.FROM(tableInfo.getTableName())
.WHERE(where)
.toString();
logger.info("sql->{},params->{}",sql,entity);
return sql;
}
public String selectBatchIds(@Param("ids")Collection<?> entity, ProviderContext context){
TableInfo tableInfo = getTableInfo(context);
String tableName = tableInfo.getTableName();
if (tableInfo.isUnionId()){
String[] where = new String[entity.size()];
for (int i = 0; i < entity.size(); i++){
List<String> list = new ArrayList<>();
String s = "%s=#{ids[%d].%s}";
for (ColumnInfo columnInfo:tableInfo.getColumns()){
if (columnInfo.isPrimaryKey()){
list.add(String.format(s,columnInfo.getColumn(),i,columnInfo.getField().getName()));
}
}
where[i] = "("+StringUtils.join(list," and ")+")";
}
String sql = "select %s from %s where %s";
sql = String.format(sql,tableInfo.selectColumnAsProperty(),tableInfo.getTableName(),StringUtils.join(where," or "));
logger.info("sql->{},params->{}",sql,entity);
return sql;
}else {
String idName = tableInfo.getColumns()
.stream()
.filter(ColumnInfo::isPrimaryKey)
.findFirst()
.get()
.getColumn();
String sql = "select %s from %s where %s in (%s) ";
String[] arr = new String[entity.size()];
for (int i = 0; i < entity.size(); i++){
arr[i] = "#{ids["+i+"]}";
}
sql = String.format(sql,tableInfo.selectColumnAsProperty(),tableName,idName,StringUtils.join(arr,","));
logger.info("sql->{},params->{}",sql,entity);
return sql;
}
}
public String selectAll(T entity, ProviderContext context){
TableInfo tableInfo = getTableInfo(context);
SQL sql = new SQL()
.SELECT(tableInfo.selectColumnAsProperty())
.FROM(tableInfo.getTableName());
String orderBy = tableInfo.orderByColumn();
if (StringUtils.isNotEmpty(orderBy)){
sql.ORDER_BY(orderBy);
}
return sql.toString();
}
public String selectPage(PageRequest<T> entity, ProviderContext context){
TableInfo tableInfo = getTableInfo(context);
SQL sql = new SQL()
.SELECT(tableInfo.selectColumnAsProperty())
.FROM(tableInfo.getTableName());
String[] where = tableInfo.getColumns().stream()
.filter(column -> {
Field field = column.getField();
T bean = entity.getPageParams();
Object value = Util.getFieldValue(bean, field);
if (value == null) {
return false;
}
return StringUtils.isNotEmpty(value.toString());
})
.map(column -> {
String param = " #{pageParams." + column.getField().getName()+"}";
if (column.getFilterOperator() == FilterOperator.LIKE){
param = "concat('%', "+param+", '%')";
}
if (column.getFilterOperator() == FilterOperator.LEFTLIKE){
param = "concat("+param+", '%')";
}
if (column.getFilterOperator() == FilterOperator.RIGHTLIKE){
param = "concat('%', "+param+")";
}
return column.getColumn()+column.filterOperator()+param;
})
.toArray(String[]::new);
sql.WHERE(where);
if (StringUtils.isNotEmpty(entity.getOrder())){
ColumnInfo columnInfo = tableInfo.getColumns().stream()
.filter(columnInfo1 -> columnInfo1.getField().getName().equalsIgnoreCase(entity.getOrder()))
.findFirst().orElse(null);
if (columnInfo != null){
String direction = entity.getOrderDirection();
direction = (StringUtils.isEmpty(direction) || direction.equalsIgnoreCase("asc"))?" asc ":" desc ";
sql.ORDER_BY(columnInfo.getColumn() + direction);
}
}else {
String orderBy = tableInfo.orderByColumn();
if (StringUtils.isNotEmpty(orderBy)){
sql.ORDER_BY(orderBy);
}
}
sql.OFFSET("#{offset}").LIMIT("#{pageSize}");
String s = sql.toString();
logger.info("sql->{},params->{}",s,entity);
return s;
}
public String selectCount(T entity, ProviderContext context){
TableInfo tableInfo = getTableInfo(context);
SQL sql = new SQL()
.SELECT("count(1)")
.FROM(tableInfo.getTableName());
String[] where = tableInfo.getColumns().stream()
.filter(column -> {
Field field = column.getField();
Object value = Util.getFieldValue(entity, field);
if (value == null) {
return false;
}
return StringUtils.isNotEmpty(value.toString());
})
.map(column -> {
String param = " #{" + column.getField().getName()+"}";
if (column.getFilterOperator() == FilterOperator.LIKE){
param = "concat('%', "+param+", '%')";
}
if (column.getFilterOperator() == FilterOperator.LEFTLIKE){
param = "concat("+param+", '%')";
}
if (column.getFilterOperator() == FilterOperator.RIGHTLIKE){
param = "concat('%', "+param+")";
}
return column.getColumn()+column.filterOperator()+param;
})
.toArray(String[]::new);
sql.WHERE(where);
String s = sql.toString();
logger.info("sql->{},params->{}",s,entity);
return s;
}
private TableInfo getTableInfo(ProviderContext context){
Class<?> clz = getEntityType(context);
return tableCache.computeIfAbsent(context.getMapperType(), t-> Util.tableInfo(clz));
}
private Class<?> getEntityType(ProviderContext context) {
return Stream.of(context.getMapperType().getGenericInterfaces())
.filter(ParameterizedType.class::isInstance)
.map(ParameterizedType.class::cast)
.filter(type -> type.getRawType() == BaseMapper.class)
.findFirst()
.map(type -> type.getActualTypeArguments()[0])
.filter(Class.class::isInstance)
.map(Class.class::cast)
.orElseThrow(() -> new IllegalStateException("未找到BaseMapper的泛型類 " + context.getMapperType().getName() + "."));
}
}5、實體類轉TableInfo
public static TableInfo tableInfo(Class<?> entityClass) {
TableInfo info = new TableInfo();
info.setEntityClass(entityClass);
Table table = entityClass.getAnnotation(Table.class);
String tableName = entityClass.getSimpleName();
if (table != null && StringUtils.isNotEmpty(table.value())){
tableName = table.value();
}
info.setTableName(tableName);
Field[] allFields = getFields(entityClass);
Field[] fields = Stream.of(allFields)
//過濾@Transient注解的field
.filter(field -> !field.isAnnotationPresent(Transient.class))
.toArray(Field[]::new);
List<ColumnInfo> columns = new ArrayList<>();
int idCount = 0;
for (Field field:fields){
ColumnInfo columnInfo = new ColumnInfo();
columnInfo.setFieldClass(field.getDeclaringClass());
columnInfo.setField(field);
Id id = field.getAnnotation(Id.class);
idCount = idCount + (id == null?0:1);
columnInfo.setPrimaryKey(id == null?Boolean.FALSE:Boolean.TRUE);
columnInfo.setPrimaryKeyAuto(id == null?Boolean.FALSE:id.auto());
Column column = field.getAnnotation(Column.class);
String columnName = field.getName();
if (column != null && StringUtils.isNotEmpty(column.value())){
columnName = column.value();
}
columnInfo.setColumn(columnName);
FilterOperator filterOperator = FilterOperator.EQ;
if (column != null && column.filterOperator() != null){
filterOperator = column.filterOperator();
}
columnInfo.setFilterOperator(filterOperator);
if (columnInfo.isPrimaryKeyAuto()){
columnInfo.setInsertable(false);
}else {
columnInfo.setInsertable(true);
if (column != null){
columnInfo.setInsertable(column.insertable());
}
}
columnInfo.setUpdatable(true);
columnInfo.setSelectable(true);
if (column != null){
columnInfo.setSelectable(column.selectable());
columnInfo.setUpdatable(column.updatable());
}
OrderBy orderBy = field.getAnnotation(OrderBy.class);
if (orderBy != null){
columnInfo.setOrderBy(orderBy.order());
columnInfo.setOrderByPriority(orderBy.orderPriority());
}
columns.add(columnInfo);
}
if (idCount > 1){
info.setUnionId(Boolean.TRUE);
}
info.setColumns(columns);
return info;
}6、字典字段自動翻譯
簡單實現(xiàn)思路:對需要翻譯的字段加上@FieldTrans注解來表明這個字段需要翻譯,通過AOP方式對結果數(shù)據(jù)進行增強,來將字段進行翻譯更新。
此部分內(nèi)容留待后續(xù)實現(xiàn),同時調(diào)研一下是否還有更優(yōu)雅簡單的實現(xiàn)方式。
7、數(shù)據(jù)權限
我們先來思考一下數(shù)據(jù)權限到底要干啥?一句話來概括:查一張表的數(shù)據(jù)時在where條件中追加“and 控制權限的列 in (???)”。
簡單實現(xiàn)方法:在控制權限的字段加上@DataAuthrity注解來表明通過這個字段控制權限,而???的內(nèi)容肯定是由業(yè)務代碼來生成的,因此考慮給這個注解增加一個屬性,用來指明權限數(shù)據(jù)由執(zhí)行哪個接口或方法來獲取。
此部分內(nèi)容留待后續(xù)實現(xiàn),同時調(diào)研一下是否還有更優(yōu)雅簡單的實現(xiàn)方式。
五、使用示例
1、數(shù)據(jù)庫表
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `age` int(11) DEFAULT NULL, `email` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
2、實體
@Table("user")
public class User {
@Id(auto = true)
@Column(value = "id")
private Long id;
@Column(value = "name", filterOperator = FilterOperator.LIKE)
@OrderBy(orderPriority = 0)
private String name;
@OrderBy(order = Order.DESC, orderPriority = 1)
private Integer age;
private String email;
@Transient
private String test;
}3、Mapper
public interface UserMapper extends BaseMapper<User, Long> {
}至此,不需要寫任何mapper.xml,UserMapper已經(jīng)具備了增刪改查能力。
4、聯(lián)合主鍵示例
public class User1 {
@Id
@Column(value = "id1")
private String id1;
@Id
@Column(value = "id2")
private String id2;
@Column(value = "name", filterOperator = FilterOperator.LIKE)
@OrderBy(orderPriority = 0)
private String name;
@OrderBy(order = Order.DESC, orderPriority = 1)
private Integer age;
private String email;
@Transient
private String test;
}
public class User1Id {
private String id1;
private String id2;
}
public interface User1Mapper extends BaseMapper<User1,User1Id> {
}六、總結
本輪子目前基本上不值一提,但相信后面我再把字典翻譯、通用數(shù)據(jù)權限加上的話,仍然會不值一提。
實際上輪子本身不重要,開發(fā)過程中的各種思考、試驗更重要吧。
以上就是詳解如何開發(fā)一個MyBatis通用Mapper的輪子的詳細內(nèi)容,更多關于MyBatis通用Mapper輪子的資料請關注腳本之家其它相關文章!
相關文章
Spring?MVC文件請求處理MultipartResolver詳解
這篇文章主要介紹了Spring?MVC文件請求處理詳解:MultipartResolver,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-11-11
Java 獲取Html文本中的img標簽下src中的內(nèi)容方法
今天小編就為大家分享一篇Java 獲取Html文本中的img標簽下src中的內(nèi)容方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-06-06
Java中的抽象工廠模式_動力節(jié)點Java學院整理
抽象工廠模式是工廠方法模式的升級版本,他用來創(chuàng)建一組相關或者相互依賴的對象。下面通過本文給大家分享Java中的抽象工廠模式,感興趣的朋友一起看看吧2017-08-08
springboot新建項目pom.xml文件第一行報錯的解決
這篇文章主要介紹了springboot新建項目pom.xml文件第一行報錯的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-01-01

