Javaで2つのオブジェクトの各フィールドの差異を出力

```MiscUtil.java
public interface MiscUtil {
    // o1に変更前オブジェクトを、o2に変更後オブジェクト(o1と同クラス)を指定
    // 新規の場合はo1にnullを指定
    default String getCompareFieldValuesString(Object o1, Object o2, List<Class<?>> skipFieldClasses, List<String> skipFieldNames) {
        try {
            Set<String> skipFieldNamesSet = new HashSet<>(skipFieldNames);
            Class<?> clazz = o1 != null ? o1.getClass() : o2.getClass();
            StringBuffer sb = new StringBuffer();
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                field.setAccessible(true);
                Class<?> fieldClass = field.getType();
                if (isAssignable(fieldClass, skipFieldClasses)) {
                    continue;
                }
                String fieldName = field.getName();
                if (skipFieldNamesSet.contains(fieldName)) {
                    continue;
                }
                Object o1Value = o1 != null ? field.get(o1) : null;
                Object o2Value = field.get(o2);
                if (isAssignable(fieldClass, Arrays.asList(String.class)) && isEmpty((String) o1Value) && isEmpty((String) o2Value)) {
                    continue; // Stringのnullと空文字は等価とする(案件独自)
                }
                if (isAssignable(fieldClass, Arrays.asList(LocalDateTime.class, LocalTime.class))) {
                    fieldName = fieldName.replaceAll("Raw$", ""); // LocalDateTime・LocalTimeのフィールド名の末尾の「Raw」は出力しない(案件独自)
                }
                String o1ValueString = getValueString(o1Value);
                String o2ValueString = getValueString(o2Value);
                if (!equals(o1ValueString, o2ValueString)) {
                    if (sb.length() > 0) {
                        sb.append(", ");
                    }
                    if (o1 != null) {
                        sb.append(String.format("%s:%s->%s", fieldName, o1ValueString, o2ValueString));
                    } else {
                        sb.append(String.format("%s:%s", fieldName, o2ValueString));
                    }
                }
            }
            return sb.toString();
        } catch (Throwable throwable) {
            throw new RuntimeException(throwable);
        }
    }

    default boolean isAssignable(Class<?> fromClass, List<Class<?>> toClasses) {
        for (Class<?> toClass : toClasses) {
            if (toClass.isAssignableFrom(fromClass)) {
                return true;
            }
        }
        return false;
    }

    default boolean isEmpty(String s) {
        return StringUtils.isEmpty(s); // nullも空文字もEmpty扱い
    }

    default boolean equals(Object o1, Object o2) {
        if (o1 != null) {
            return o1.equals(o2);
        }
        return false; // 両方nullでもfalse(trueとすべき？)
    }

    default String getValueString(Object o) {
        if (o == null) {
            return "null";
        }
        if (o instanceof String) {
            String s = (String) o;
            return String.format("\"%s\"", escape(s));
        }
        if (o instanceof LocalDateTime) {
            return toStr(plus9Hours((LocalDateTime) o)); // 9時間足す(案件独自)
        }
        if (o instanceof LocalTime) {
            return toStr(plus9Hours((LocalTime) o)); // 9時間足す(案件独自)
        }
        if (o instanceof LocalDate) {
            return toStr((LocalDate) o);
        }
        return String.format("%s", o.toString());
    }

    default String escape(String s) {
        return s.replaceAll("\n", "\\\\n"); // 改行は\\nに変換
    }

    static final String DATE_FORMAT = "yyyy/MM/dd(E)";
    static final String TIME_FORMAT = "HH:mm";
    static final String DATETIME_FORMAT = DATE_FORMAT + " " + TIME_FORMAT;

    default String toStr(LocalDateTime datetime) {
        return datetime != null ? DateTimeFormatter.ofPattern(DATETIME_FORMAT, Locale.JAPANESE).format(datetime) : null;
    }

    default String toStr(LocalDate date) {
        return date != null ? DateTimeFormatter.ofPattern(DATE_FORMAT, Locale.JAPANESE).format(date) : null;
    }

    default String toStr(LocalTime time) {
        return time != null ? DateTimeFormatter.ofPattern(TIME_FORMAT, Locale.JAPANESE).format(time) : null;
    }

    default LocalDateTime plus9Hours(LocalDateTime localDateTime) {
        return localDateTime != null ? localDateTime.plusHours(9) : null;
    }

    default LocalTime plus9Hours(LocalTime localTime) {
        return localTime != null ? localTime.plusHours(9) : null;
    }
}
```