(1). 概述

前面对Liquibase进行了简单的集成,心里有一个疑问,Liquibase是如何把ChangLog(XML/JSON/YAML/SQLFile)进行解析,并执行的?
原理就在ChangeLogParser

(2). ChangeLogParser部析

从类图上就能看出来,ChangeLogParser支持:XML/JSON/YAML/SQLFile,我只对XML的实现感兴趣.

"ChangeLogParser"

(3). AbstractChangeLogParser

public abstract class AbstractChangeLogParser implements ChangeLogParser {

    @Override
    public DatabaseChangeLog parse(String physicalChangeLogLocation, ChangeLogParameters changeLogParameters,
                                   ResourceAccessor resourceAccessor) throws ChangeLogParseException {
        
		// *************************************************************************************
		// 1. 委托给子类:XMLChangeLogSAXParser对xml进行解析.
		// *************************************************************************************
		ParsedNode parsedNode = parseToNode(physicalChangeLogLocation, changeLogParameters, resourceAccessor);
        if (parsedNode == null) {
            return null;
        }


		// *************************************************************************************
		// 2. 委托给:DatabaseChangeLog对xml解析后的内容,进行深度解析成业务模型
		// *************************************************************************************
        DatabaseChangeLog changeLog = new DatabaseChangeLog(physicalChangeLogLocation);
        changeLog.setChangeLogParameters(changeLogParameters);
        try {
			// 
            changeLog.load(parsedNode, resourceAccessor);
        } catch (Exception e) {
            throw new ChangeLogParseException(e);
        }

        return changeLog;
    }

    protected abstract ParsedNode parseToNode(String physicalChangeLogLocation, ChangeLogParameters changeLogParameters,
                                              ResourceAccessor resourceAccessor) throws ChangeLogParseException;
}

(4). XML解析入口:XMLChangeLogSAXParser

public class XMLChangeLogSAXParser extends AbstractChangeLogParser {
    
	public static final String LIQUIBASE_SCHEMA_VERSION = "3.6";
    private static final boolean PREFER_INTERNAL_XSD = Boolean.getBoolean("liquibase.prefer.internal.xsd");
    private static final String XSD_FILE = "dbchangelog-" + LIQUIBASE_SCHEMA_VERSION + ".xsd";
    private SAXParserFactory saxParserFactory;

    public XMLChangeLogSAXParser() {
        saxParserFactory = SAXParserFactory.newInstance();
        saxParserFactory.setValidating(true);
        saxParserFactory.setNamespaceAware(true);
        
        if (PREFER_INTERNAL_XSD) {
            InputStream xsdInputStream = XMLChangeLogSAXParser.class.getResourceAsStream(XSD_FILE);
            if (xsdInputStream != null) {
                try {
                    SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
                    Schema schema = schemaFactory.newSchema(new StreamSource(xsdInputStream));
                    saxParserFactory.setSchema(schema);
                    saxParserFactory.setValidating(false);
                } catch (SAXException e) {
                    LogService.getLog(XMLChangeLogSAXParser.class).warning("Could not load " + XSD_FILE + ", enabling parser validator", e);
                }
            }
        }
    }

    
    //  *********************************************************************************************************
	// 1. 对xml文件进行解析.
	//  *********************************************************************************************************
	@Override
    protected ParsedNode parseToNode(String physicalChangeLogLocation, ChangeLogParameters changeLogParameters,ResourceAccessor resourceAccessor) throws ChangeLogParseException {
        try (
			// 加载xml文佣
            InputStream inputStream = StreamUtil.singleInputStream(physicalChangeLogLocation, resourceAccessor)) {
            SAXParser parser = saxParserFactory.newSAXParser();
            trySetSchemaLanguageProperty(parser);
    
            XMLReader xmlReader = parser.getXMLReader();
            LiquibaseEntityResolver resolver=new LiquibaseEntityResolver(this);
            resolver.useResoureAccessor(resourceAccessor,FilenameUtils.getFullPath(physicalChangeLogLocation));
            xmlReader.setEntityResolver(resolver);
            xmlReader.setErrorHandler(new ErrorHandler() {
                @Override
                public void warning(SAXParseException exception) throws SAXException {
                    LogService.getLog(getClass()).warning(LogType.LOG, exception.getMessage());
                    throw exception;
                }

                @Override
                public void error(SAXParseException exception) throws SAXException {
                    LogService.getLog(getClass()).severe(LogType.LOG, exception.getMessage());
                    throw exception;
                }

                @Override
                public void fatalError(SAXParseException exception) throws SAXException {
                    LogService.getLog(getClass()).severe(LogType.LOG, exception.getMessage());
                    throw exception;
                }
            });
        	
            if (inputStream == null) {
                if (physicalChangeLogLocation.startsWith("WEB-INF/classes/")) {
                    // Correct physicalChangeLogLocation and try again.
                    return parseToNode(
                        physicalChangeLogLocation.replaceFirst("WEB-INF/classes/", ""),
                            changeLogParameters, resourceAccessor);
                } else {
                    throw new ChangeLogParseException(physicalChangeLogLocation + " does not exist");
                }
            }

            XMLChangeLogSAXHandler contentHandler = new XMLChangeLogSAXHandler(physicalChangeLogLocation, resourceAccessor, changeLogParameters);
            xmlReader.setContentHandler(contentHandler);
            xmlReader.parse(new InputSource(new UtfBomStripperInputStream(inputStream)));

            return contentHandler.getDatabaseChangeLogTree();
        } catch (ChangeLogParseException e) {
            throw e;
        } catch (IOException e) {
            throw new ChangeLogParseException("Error Reading Migration File: " + e.getMessage(), e);
        } catch (SAXParseException e) {
            throw new ChangeLogParseException("Error parsing line " + e.getLineNumber() + " column " + e.getColumnNumber() + " of " + physicalChangeLogLocation +": " + e.getMessage(), e);
        } catch (SAXException e) {
            Throwable parentCause = e.getException();
            while (parentCause != null) {
                if (parentCause instanceof ChangeLogParseException) {
                    throw ((ChangeLogParseException) parentCause);
                }
                parentCause = parentCause.getCause();
            }
            String reason = e.getMessage();
            String causeReason = null;
            if (e.getCause() != null) {
                causeReason = e.getCause().getMessage();
            }
            if (reason == null) {
                if (causeReason != null) {
                    reason = causeReason;
                } else {
                    reason = "Unknown Reason";
                }
            }

            throw new ChangeLogParseException("Invalid Migration File: " + reason, e);
        } catch (Exception e) {
            throw new ChangeLogParseException(e);
        }
    } // end parseToNode

}

(5). 解析XML转换成DatabaseChangeLog

public class DatabaseChangeLog implements Comparable<DatabaseChangeLog>, Conditional {

   // *****************************************************************************
   // 1. 解析xml内容,并转换到DatabaseChangeLog里
   // *****************************************************************************
	public void load(ParsedNode parsedNode, ResourceAccessor resourceAccessor)
            throws ParsedNodeException, SetupException {
        
		// 1.1 获得日志文件的详细路径
		setLogicalFilePath(parsedNode.getChildValue(null, "logicalFilePath", String.class));

		// 1.2 解析context
        setContexts(new ContextExpression(parsedNode.getChildValue(null, "context", String.class)));
        String objectQuotingStrategy = parsedNode.getChildValue(null, "objectQuotingStrategy", String.class);
        if (objectQuotingStrategy != null) {
            setObjectQuotingStrategy(ObjectQuotingStrategy.valueOf(objectQuotingStrategy));
        }

		// 1.3 遍历所有的xml子节点
        for (ParsedNode childNode : parsedNode.getChildren()) {
            handleChildNode(childNode, resourceAccessor);
        }
    } //end load

    
   
	protected void handleChildNode(ParsedNode node, ResourceAccessor resourceAccessor)
            throws ParsedNodeException, SetupException {
        expandExpressions(node);

        String nodeName = node.getName();
        switch (nodeName) {  
            //  *************************************************************************************************
			// 1.4 对<changeSet>进行解析
			//  *************************************************************************************************
			case"changeSet":  
                // 如果dbms match
				if (isDbmsMatch(node.getChildValue(null, "dbms", String.class))) {
					// 
					this.addChangeSet(createChangeSet(node, resourceAccessor));
				}
                break;
            //  *************************************************************************************************
			// 1.5 对<include>进行解析
			//  *************************************************************************************************
			case"include": {
            String path = node.getChildValue(null, "file", String.class);
            if (path == null) {
                throw new UnexpectedLiquibaseException("No 'file' attribute on 'include'");
            }
            path = path.replace('\\', '/');
            ContextExpression includeContexts = new ContextExpression(node.getChildValue(null, "context", String.class));
            try {
                include(path, node.getChildValue(null, "relativeToChangelogFile", false), resourceAccessor, includeContexts, true);
            } catch (LiquibaseException e) {
                throw new SetupException(e);}
                break;
            }
			//  *************************************************************************************************
			// 1.6 对<includeAll>进行解析
			//  *************************************************************************************************
            case "includeAll": {
                String path = node.getChildValue(null, "path", String.class);
                String resourceFilterDef = node.getChildValue(null, "filter", String.class);
                if (resourceFilterDef == null) {
                    resourceFilterDef = node.getChildValue(null, "resourceFilter", String.class);
                }
                IncludeAllFilter resourceFilter = null;
                if (resourceFilterDef != null) {
                    try {
                        resourceFilter = (IncludeAllFilter) Class.forName(resourceFilterDef).getConstructor().newInstance();
                    } catch (ReflectiveOperationException e) {
                        throw new SetupException(e);
                    }
                }

                String resourceComparatorDef = node.getChildValue(null, "resourceComparator", String.class);
                Comparator<String> resourceComparator = null;
                if (resourceComparatorDef != null) {
                    try {
                        resourceComparator = (Comparator<String>) Class.forName(resourceComparatorDef).getConstructor().newInstance();
                    } catch (ReflectiveOperationException e) {
                        //take default comparator
                        LogService.getLog(getClass()).info(LogType.LOG, "no resourceComparator defined - taking default " +
                         "implementation");
                        resourceComparator = getStandardChangeLogComparator();
                    }
                }

                ContextExpression includeContexts = new ContextExpression(node.getChildValue(null, "context", String.class));
                includeAll(path, node.getChildValue(null, "relativeToChangelogFile", false), resourceFilter,
                        node.getChildValue(null, "errorIfMissingOrEmpty", true),
                        resourceComparator, resourceAccessor, includeContexts);
                break;
            }
			//  *************************************************************************************************
			// 1.7 对<preConditions>进行解析
			//  *************************************************************************************************
            case "preConditions": {
                this.preconditionContainer = new PreconditionContainer();
                try {
                    this.preconditionContainer.load(node, resourceAccessor);
                } catch (ParsedNodeException e) {
                    e.printStackTrace();
                }
                break;
            }

			//  *************************************************************************************************
			// 1.8 对<property>进行解析
			//  *************************************************************************************************
            case "property": {
                try {
                    String context = node.getChildValue(null, "context", String.class);
                    String dbms = node.getChildValue(null, "dbms", String.class);
                    String labels = node.getChildValue(null, "labels", String.class);
                    Boolean global = node.getChildValue(null, "global", Boolean.class);
                    if (global == null) {
                        // okay behave like liquibase < 3.4 and set global == true
                        global = true;
                    }

                    String file = node.getChildValue(null, "file", String.class);

                    if (file == null) {
                        // direct referenced property, no file
                        String name = node.getChildValue(null, "name", String.class);
                        String value = node.getChildValue(null, "value", String.class);

                        this.changeLogParameters.set(name, value, context, labels, dbms, global, this);
                    } else {
                        // read properties from the file
                        Properties props = new Properties();
                        InputStream propertiesStream = StreamUtil.singleInputStream(file, resourceAccessor);
                        if (propertiesStream == null) {
                            LogService.getLog(getClass()).info(LogType.LOG, "Could not open properties file " + file);
                        } else {
                            props.load(propertiesStream);

                            for (Map.Entry entry : props.entrySet()) {
                                this.changeLogParameters.set(
                                        entry.getKey().toString(),
                                        entry.getValue().toString(),
                                        context,
                                        labels,
                                        dbms,
                                        global,
                                        this
                                );
                            }
                        }
                    }
                } catch (IOException e) {
                    throw new ParsedNodeException(e);
                }

                break;
            }
        }
    } // end handleChildNode


	// ****************************************************************
	// 1.4.1 对<changeSet>进行解析,并转换成:ChangeSet对象
	// ****************************************************************
	protected ChangeSet createChangeSet(ParsedNode node, ResourceAccessor resourceAccessor) throws ParsedNodeException {
		ChangeSet changeSet = new ChangeSet(this);
		changeSet.setChangeLogParameters(this.getChangeLogParameters());
		changeSet.load(node, resourceAccessor);
		return changeSet;
	}
}	

(6). ChangeSet对象

public class ChangeSet implements Conditional, ChangeLogChild {

    // ********************************************************************************
	// 1. 创建ChangeSet,解析xml,并把xml内容转换到:ChangeSet对象里.
	// ********************************************************************************
    public void load(ParsedNode node, ResourceAccessor resourceAccessor) throws ParsedNodeException {
		// <changeSet id="1">
        this.id = node.getChildValue(null, "id", String.class);
		// <changeSet author="nvoxland">
        this.author = node.getChildValue(null, "author", String.class);
		// runAlways
        this.alwaysRun  = node.getChildValue(null, "runAlways", node.getChildValue(null, "alwaysRun", false));
		// runOnChange
        this.runOnChange  = node.getChildValue(null, "runOnChange", false);
		// context
        this.contexts = new ContextExpression(node.getChildValue(null, "context", String.class));
		// 
        this.labels = new Labels(StringUtils.trimToNull(node.getChildValue(null, "labels", String.class)));

		// <changeSet id="1" author="nvoxland" dbms="" >
        setDbms(node.getChildValue(null, "dbms", String.class));
		// 
        this.runInTransaction  = node.getChildValue(null, "runInTransaction", true);
        this.created = node.getChildValue(null, "created", String.class);
        this.runOrder = node.getChildValue(null, "runOrder", String.class);
        this.ignore = node.getChildValue(null, "ignore", false);
        this.comments = StringUtils.join(node.getChildren(null, "comment"), "\n", new StringUtils.StringUtilsFormatter() {
            @Override
            public String toString(Object obj) {
                if (((ParsedNode) obj).getValue() == null) {
                    return "";
                } else {
                    return ((ParsedNode) obj).getValue().toString();
                }
            }
        });
        this.comments = StringUtils.trimToNull(this.comments);

        String objectQuotingStrategyString = StringUtils.trimToNull(node.getChildValue(null, "objectQuotingStrategy", String.class));
        if (changeLog != null) {
            this.objectQuotingStrategy = changeLog.getObjectQuotingStrategy();
        }
        if (objectQuotingStrategyString != null) {
            this.objectQuotingStrategy = ObjectQuotingStrategy.valueOf(objectQuotingStrategyString);
        }

        if (this.objectQuotingStrategy == null) {
            this.objectQuotingStrategy = ObjectQuotingStrategy.LEGACY;
        }

        // xml文件位置
        this.filePath = StringUtils.trimToNull(node.getChildValue(null, "logicalFilePath", String.class));
        if (filePath == null) {
            filePath = changeLog.getFilePath();
        }

        this.setFailOnError(node.getChildValue(null, "failOnError", Boolean.class));
        String onValidationFailString = node.getChildValue(null, "onValidationFail", "HALT");
        this.setOnValidationFail(ValidationFailOption.valueOf(onValidationFailString));

        for (ParsedNode child : node.getChildren()) {  // 循环解析余下的所有子节点
		    // ********************************************************************************
			// 2. 解析<changeSet> ...  </changeSet>下所有的内容.
			// ********************************************************************************
            handleChildNode(child, resourceAccessor);
        }
    } // end load

    // ********************************************************************************
	// 3. 解析<changeSet>下所有内容.
	// ********************************************************************************
	protected void handleChildNode(ParsedNode child, ResourceAccessor resourceAccessor) throws ParsedNodeException {
        switch (child.getName()) {
            case "rollback":    // 解析rollback
                handleRollbackNode(child, resourceAccessor);
                break;
            case "validCheckSum":
            case "validCheckSums":   // 解析validCheckSums
                if (child.getValue() == null) {
                    return;
                }

                if (child.getValue() instanceof Collection) {
                    for (Object checksum : (Collection) child.getValue()) {
                        addValidCheckSum((String) checksum);
                    }
                } else {
                    addValidCheckSum(child.getValue(String.class));
                }
                break;
            case "modifySql":   // 解析modifySql
                String dbmsString = StringUtils.trimToNull(child.getChildValue(null, "dbms", String.class));
                String contextString = StringUtils.trimToNull(child.getChildValue(null, "context", String.class));
                String labelsString = StringUtils.trimToNull(child.getChildValue(null, "labels", String.class));
                boolean applyToRollback = child.getChildValue(null, "applyToRollback", false);

                Set<String> dbms = new HashSet<>();
                if (dbmsString != null) {
                    dbms.addAll(StringUtils.splitAndTrim(dbmsString, ","));
                }
                ContextExpression context = null;
                if (contextString != null) {
                    context = new ContextExpression(contextString);
                }

                Labels labels = null;
                if (labelsString != null) {
                    labels = new Labels(labelsString);
                }


                List<ParsedNode> potentialVisitors = child.getChildren();
                for (ParsedNode node : potentialVisitors) {
                    SqlVisitor sqlVisitor = SqlVisitorFactory.getInstance().create(node.getName());
                    if (sqlVisitor != null) {
                        sqlVisitor.setApplyToRollback(applyToRollback);
                        if (!dbms.isEmpty()) {
                            sqlVisitor.setApplicableDbms(dbms);
                        }
                        sqlVisitor.setContexts(context);
                        sqlVisitor.setLabels(labels);
                        sqlVisitor.load(node, resourceAccessor);

                        addSqlVisitor(sqlVisitor);
                    }
                }


                break;
            case "preConditions":  // 解析preConditions
                this.preconditions = new PreconditionContainer();
                try {
                    this.preconditions.load(child, resourceAccessor);
                } catch (ParsedNodeException e) {
                    e.printStackTrace();
                }
                break;
            case "changes":   // 解析changes
                for (ParsedNode changeNode : child.getChildren()) {
                    handleChildNode(changeNode, resourceAccessor);
                }
                break;
            default:        
			    // *********************************************************************************
				// 4. 解析其它(createTable/addColumn/insert/...)标签.
				// *********************************************************************************
                Change change = toChange(child, resourceAccessor);
                if ((change == null) && (child.getValue() instanceof String)) {
                    this.setAttribute(child.getName(), child.getValue());
                } else {
                    addChange(change);
                }
                break;
        }
    }// end handleChildNode

    
	// ************************************************************************
	// 5. 解析其它(createTable/addColumn/insert/...)标签,通过Change进行承载
	// ************************************************************************
	protected Change toChange(ParsedNode value, ResourceAccessor resourceAccessor) throws ParsedNodeException {
		// **********************************************************************
		// 通过SPI,加载(liquibase.change.Change)的所有实现类,这样就能实现动态解析XML的标签了
        // **********************************************************************
		Change change = ChangeFactory.getInstance().create(value.getName());
        if (change == null) {
            return null;
        } else {
			// **********************************************************************
			// 调用Change.load方法,解析所有的标签,在这里以:InsertDataChange为案例
			// **********************************************************************
            change.load(value, resourceAccessor);

            return change;
        }
    } // end toChange

}

(7). InsertDataChange

<!-- 对insert标签进行解析 -->
<changeSet id="5" author="nvoxland" context="test">
        <insert tableName="person">
            <column name="firstname" value="John"/>
            <column name="lastname" value="Doe"/>
            <column name="username" value="jdoe"/>
        </insert>
</changeSet>
package liquibase.change.core;

@DatabaseChange(name="insert", description = "Inserts data into an existing table", priority = ChangeMetaData.PRIORITY_DEFAULT, appliesTo = "table")
public class InsertDataChange extends AbstractChange implements ChangeWithColumns<ColumnConfig>, DbmsTargetedChange {

    private String catalogName;
    private String schemaName;
    private String tableName;
    private List<ColumnConfig> columns;
    private String dbms;

    // ... 
}

(9). 总结

通过对源码的剖析,我们能知道Liquibase的业务模型(如下图),后面,会继续对Liquibase是如何执行SQL进行剖析.

"Liquibase数据模型"