Quellcode durchsuchen

实现基本功能

zhou-hao vor 3 Jahren
Ursprung
Commit
d5a01ce922

+ 1 - 1
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/entity/factory/MapperEntityFactory.java

@@ -172,7 +172,7 @@ public class MapperEntityFactory implements EntityFactory, BeanFactory {
             return (T) new HashSet<>();
         }
 
-        throw new NotFoundException("无法初始化实体类:"+beanClass);
+        throw new NotFoundException("can not create instance:"+beanClass);
     }
 
     @Override

+ 4 - 4
hsweb-commons/hsweb-commons-crud/src/main/java/org/hswebframework/web/crud/web/CommonErrorControllerAdvice.java

@@ -186,7 +186,7 @@ public class CommonErrorControllerAdvice {
     @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
     public Mono<ResponseMessage<Object>> handleException(UnsupportedMediaTypeStatusException e) {
         return LocaleUtils
-                .resolveMessage(messageSource, "error.unsupported_media_type")
+                .resolveMessageReactive(messageSource, "error.unsupported_media_type")
                 .map(msg -> ResponseMessage
                         .error(415, "unsupported_media_type", msg)
                         .result(e.getSupportedMediaTypes()))
@@ -198,7 +198,7 @@ public class CommonErrorControllerAdvice {
     public Mono<ResponseMessage<Object>> handleException(NotAcceptableStatusException e) {
 
         return LocaleUtils
-                .resolveMessage(messageSource, "error.not_acceptable_media_type")
+                .resolveMessageReactive(messageSource, "error.not_acceptable_media_type")
                 .map(msg -> ResponseMessage
                         .error(406, "not_acceptable_media_type", msg)
                         .result(e.getSupportedMediaTypes()))
@@ -209,7 +209,7 @@ public class CommonErrorControllerAdvice {
     @ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
     public Mono<ResponseMessage<Object>> handleException(MethodNotAllowedException e) {
         return LocaleUtils
-                .resolveMessage(messageSource, "error.method_not_allowed")
+                .resolveMessageReactive(messageSource, "error.method_not_allowed")
                 .map(msg -> ResponseMessage
                         .error(406, "method_not_allowed", msg)
                         .result(e.getSupportedMethods()))
@@ -228,7 +228,7 @@ public class CommonErrorControllerAdvice {
             log.warn(e.getMessage(), e);
         }
         return LocaleUtils
-                .resolveMessage(messageSource, code)
+                .resolveMessageReactive(messageSource, code)
                 .map(msg -> ResponseMessage.error(400, code, msg));
     }
 

+ 1 - 1
hsweb-commons/hsweb-commons-crud/src/main/resources/i18n/commons/messages_zh_CN.properties

@@ -1,5 +1,5 @@
 error.unsupported_media_type=不支持的请求类型
-error.not_acceptable_media_type=不支持的响应类型
+error.not_acceptable_media_type=不支持的媒体类型
 error.method_not_allowed=不支持的请求方法
 error.duplicate_data=重复的数据
 error.data_error=数据错误

+ 11 - 3
hsweb-core/src/main/java/org/hswebframework/web/dict/EnumDict.java

@@ -8,7 +8,6 @@ import com.alibaba.fastjson.parser.JSONToken;
 import com.alibaba.fastjson.parser.deserializer.ObjectDeserializer;
 import com.alibaba.fastjson.serializer.JSONSerializable;
 import com.alibaba.fastjson.serializer.JSONSerializer;
-import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonValue;
 import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -21,6 +20,7 @@ import lombok.NoArgsConstructor;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 import org.hswebframework.web.exception.ValidationException;
+import org.hswebframework.web.i18n.LocaleUtils;
 import org.springframework.beans.BeanUtils;
 import org.springframework.util.StringUtils;
 
@@ -260,6 +260,14 @@ public interface EnumDict<V> extends JSONSerializable {
         return DEFAULT_WRITE_JSON_OBJECT;
     }
 
+    default String getI18nCode() {
+        return getText();
+    }
+
+    default String getI18nMessage(Locale locale) {
+        return LocaleUtils.resolveMessage(getI18nCode(), locale, getText());
+    }
+
     /**
      * 当{@link this#isWriteJSONObjectEnabled()}返回true时,在序列化为json的时候,会写出此方法返回的对象
      *
@@ -271,7 +279,7 @@ public interface EnumDict<V> extends JSONSerializable {
         if (isWriteJSONObjectEnabled()) {
             Map<String, Object> jsonObject = new HashMap<>();
             jsonObject.put("value", getValue());
-            jsonObject.put("text", getText());
+            jsonObject.put("text", getI18nMessage(LocaleUtils.current()));
             // jsonObject.put("index", index());
             // jsonObject.put("mask", getMask());
             return jsonObject;
@@ -281,7 +289,7 @@ public interface EnumDict<V> extends JSONSerializable {
     }
 
     @Override
-    default void write(JSONSerializer jsonSerializer, Object o, Type type, int i) throws IOException {
+    default void write(JSONSerializer jsonSerializer, Object o, Type type, int i) {
         if (isWriteJSONObjectEnabled()) {
             jsonSerializer.write(getWriteJSONObject());
         } else {

+ 103 - 11
hsweb-core/src/main/java/org/hswebframework/web/i18n/LocaleUtils.java

@@ -10,25 +10,86 @@ import java.util.Locale;
 import java.util.function.BiFunction;
 import java.util.function.Function;
 
+/**
+ * 用于进行国际化消息转换
+ *
+ * @author zhouhao
+ * @since 4.0.11
+ */
 public class LocaleUtils {
 
     public static final LocaleContext DEFAULT_CONTEXT = new SimpleLocaleContext(Locale.getDefault());
 
-    public static Mono<LocaleContext> reactive() {
+    private static final ThreadLocal<LocaleContext> CONTEXT_THREAD_LOCAL = new ThreadLocal<>();
+
+    static MessageSource messageSource;
+
+    /**
+     * 获取当前的语言地区,如果没有设置则返回系统默认语言
+     *
+     * @return Locale
+     */
+    public static Locale current() {
+        LocaleContext context = CONTEXT_THREAD_LOCAL.get();
+        if (context == null || context.getLocale() == null) {
+            context = DEFAULT_CONTEXT;
+        }
+        return context.getLocale();
+    }
+
+    /**
+     * 在指定的语言环境中执行函数,<b>只能</b>在非响应式同步操作时使用,如:转换实体类中某些属性的国际化消息。
+     * <p>
+     * 在函数的逻辑中可以通过{@link LocaleUtils#current()}来获取当前语言.
+     *
+     * @param data   参数
+     * @param locale 语言地区
+     * @param mapper 函数
+     * @param <T>    参数类型
+     * @param <R>    函数返回类型
+     * @return 返回值
+     */
+    public static <T, R> R doWith(T data, Locale locale, BiFunction<T, Locale, R> mapper) {
+        try {
+            CONTEXT_THREAD_LOCAL.set(new SimpleLocaleContext(locale));
+            return mapper.apply(data, locale);
+        } finally {
+            CONTEXT_THREAD_LOCAL.remove();
+        }
+    }
+
+    /**
+     * 响应式方式获取当前语言地区
+     * @return 语言地区
+     */
+    public static Mono<Locale> currentReactive() {
         return Mono
                 .subscriberContext()
                 .map(ctx -> ctx
                         .<LocaleContext>getOrEmpty(LocaleContext.class)
-                        .orElse(DEFAULT_CONTEXT));
+                        .map(LocaleContext::getLocale)
+                        .orElseGet(Locale::getDefault)
+                );
     }
 
 
+    public static <S extends I18nSupportException, R> Mono<R> resolveThrowable(S source,
+                                                                               BiFunction<S, String, R> mapper) {
+        return resolveThrowable(messageSource, source, mapper);
+    }
+
     public static <S extends I18nSupportException, R> Mono<R> resolveThrowable(MessageSource messageSource,
                                                                                S source,
                                                                                BiFunction<S, String, R> mapper) {
         return doWithReactive(messageSource, source, Throwable::getMessage, mapper, source.getArgs());
     }
 
+    public static <S extends Throwable, R> Mono<R> resolveThrowable(S source,
+                                                                    BiFunction<S, String, R> mapper,
+                                                                    Object... args) {
+        return resolveThrowable(messageSource, source, mapper, args);
+    }
+
     public static <S extends Throwable, R> Mono<R> resolveThrowable(MessageSource messageSource,
                                                                     S source,
                                                                     BiFunction<S, String, R> mapper,
@@ -36,32 +97,63 @@ public class LocaleUtils {
         return doWithReactive(messageSource, source, Throwable::getMessage, mapper, args);
     }
 
+    public static <S, R> Mono<R> doWithReactive(S source,
+                                                Function<S, String> message,
+                                                BiFunction<S, String, R> mapper,
+                                                Object... args) {
+        return doWithReactive(messageSource, source, message, mapper, args);
+    }
+
     public static <S, R> Mono<R> doWithReactive(MessageSource messageSource,
                                                 S source,
                                                 Function<S, String> message,
                                                 BiFunction<S, String, R> mapper,
                                                 Object... args) {
-        return reactive()
-                .map(ctx -> {
+        return currentReactive()
+                .map(locale -> {
                     String msg = message.apply(source);
-                    String newMsg = resolveMessage(messageSource, msg, ctx.getLocale(), msg, args);
+                    String newMsg = resolveMessage(messageSource, locale, msg, msg, args);
                     return mapper.apply(source, newMsg);
                 });
     }
 
-    public static Mono<String> resolveMessage(MessageSource messageSource,
-                                              String code,
-                                              Object... args) {
-        return reactive()
-                .map(ctx -> resolveMessage(messageSource, code, ctx.getLocale(), code, args));
+    public static Mono<String> resolveMessageReactive(MessageSource messageSource,
+                                                      String code,
+                                                      Object... args) {
+        return currentReactive()
+                .map(locale -> resolveMessage(messageSource, locale, code, code, args));
+    }
+
+    public static String resolveMessage(String code,
+                                        Locale locale,
+                                        String defaultMessage,
+                                        Object... args) {
+        return resolveMessage(messageSource, locale, code, defaultMessage, args);
     }
 
     public static String resolveMessage(MessageSource messageSource,
-                                        String code,
                                         Locale locale,
+                                        String code,
                                         String defaultMessage,
                                         Object... args) {
         return messageSource.getMessage(code, args, defaultMessage, locale);
     }
 
+    public static String resolveMessage(String code, Object... args) {
+        return resolveMessage(messageSource, current(), code, code, args);
+    }
+
+    public static String resolveMessage(String code,
+                                        String defaultMessage,
+                                        Object... args) {
+        return resolveMessage(messageSource, current(), code, defaultMessage, args);
+    }
+
+    public static String resolveMessage(MessageSource messageSource,
+                                        String code,
+                                        String defaultMessage,
+                                        Object... args) {
+        return resolveMessage(messageSource, current(), code, defaultMessage, args);
+    }
+
 }

+ 12 - 0
hsweb-core/src/main/java/org/hswebframework/web/i18n/MessageSourceInitializer.java

@@ -0,0 +1,12 @@
+package org.hswebframework.web.i18n;
+
+import org.springframework.context.MessageSource;
+
+public class MessageSourceInitializer {
+
+    public static void init(MessageSource messageSource) {
+        if (LocaleUtils.messageSource == null) {
+            LocaleUtils.messageSource = messageSource;
+        }
+    }
+}

+ 3 - 2
hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/CompositeMessageSource.java

@@ -10,6 +10,7 @@ import javax.annotation.Nonnull;
 import java.util.Collection;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 import java.util.concurrent.CopyOnWriteArrayList;
 
 public class CompositeMessageSource implements MessageSource {
@@ -27,12 +28,12 @@ public class CompositeMessageSource implements MessageSource {
     @Override
     public String getMessage(@Nonnull String code, Object[] args, String defaultMessage, @Nonnull Locale locale) {
         for (MessageSource messageSource : messageSources) {
-            String result = messageSource.getMessage(code, args, defaultMessage, locale);
+            String result = messageSource.getMessage(code, args, null, locale);
             if (StringUtils.hasText(result)) {
                 return result;
             }
         }
-        return null;
+        return defaultMessage;
     }
 
     @Override

+ 2 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/i18n/I18nConfiguration.java

@@ -1,6 +1,7 @@
 package org.hswebframework.web.starter.i18n;
 
 import org.hswebframework.web.exception.BusinessException;
+import org.hswebframework.web.i18n.MessageSourceInitializer;
 import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.boot.autoconfigure.AutoConfigureOrder;
 import org.springframework.context.MessageSource;
@@ -31,6 +32,7 @@ public class I18nConfiguration {
     public MessageSource compositeMessageSource(ObjectProvider<MessageSource> objectProvider) {
         CompositeMessageSource messageSource = new CompositeMessageSource();
         messageSource.addMessageSources(objectProvider.stream().collect(Collectors.toList()));
+        MessageSourceInitializer.init(messageSource);
         return messageSource;
     }
 

+ 1 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomCodecsAutoConfiguration.java

@@ -51,6 +51,7 @@ public class CustomCodecsAutoConfiguration {
             return (configurer) -> {
                 CodecConfigurer.DefaultCodecs defaults = configurer.defaultCodecs();
                 defaults.jackson2JsonDecoder(new CustomJackson2JsonDecoder(entityFactory, objectMapper));
+                defaults.jackson2JsonEncoder(new CustomJackson2jsonEncoder(objectMapper));
             };
         }
 

+ 340 - 0
hsweb-starter/src/main/java/org/hswebframework/web/starter/jackson/CustomJackson2jsonEncoder.java

@@ -0,0 +1,340 @@
+package org.hswebframework.web.starter.jackson;
+
+import org.hswebframework.web.i18n.LocaleUtils;
+import org.springframework.http.codec.json.Jackson2CodecSupport;
+
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.nio.charset.Charset;
+import java.util.*;
+
+import com.fasterxml.jackson.core.JsonEncoding;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.util.ByteArrayBuilder;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.fasterxml.jackson.databind.SequenceWriter;
+import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import org.springframework.core.MethodParameter;
+import org.springframework.core.ResolvableType;
+import org.springframework.core.codec.CodecException;
+import org.springframework.core.codec.EncodingException;
+import org.springframework.core.codec.Hints;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferFactory;
+import org.springframework.core.log.LogFormatUtils;
+import org.springframework.http.MediaType;
+import org.springframework.http.codec.HttpMessageEncoder;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.MimeType;
+
+/**
+ * Base class providing support methods for Jackson 2.9 encoding. For non-streaming use
+ * cases, {@link Flux} elements are collected into a {@link List} before serialization for
+ * performance reason.
+ *
+ * @author Sebastien Deleuze
+ * @author Arjen Poutsma
+ * @since 5.0
+ */
+public class CustomJackson2jsonEncoder extends Jackson2CodecSupport implements HttpMessageEncoder<Object> {
+
+    private static final byte[] NEWLINE_SEPARATOR = {'\n'};
+
+    private static final Map<MediaType, byte[]> STREAM_SEPARATORS;
+
+    private static final Map<String, JsonEncoding> ENCODINGS;
+
+    static {
+        STREAM_SEPARATORS = new HashMap<>(4);
+        STREAM_SEPARATORS.put(MediaType.APPLICATION_STREAM_JSON, NEWLINE_SEPARATOR);
+        STREAM_SEPARATORS.put(MediaType.parseMediaType("application/stream+x-jackson-smile"), new byte[0]);
+
+        ENCODINGS = new HashMap<>(JsonEncoding.values().length + 1);
+        for (JsonEncoding encoding : JsonEncoding.values()) {
+            ENCODINGS.put(encoding.getJavaName(), encoding);
+        }
+        ENCODINGS.put("US-ASCII", JsonEncoding.UTF8);
+    }
+
+
+    private final List<MediaType> streamingMediaTypes = new ArrayList<>(1);
+
+
+    /**
+     * Constructor with a Jackson {@link ObjectMapper} to use.
+     */
+    protected CustomJackson2jsonEncoder(ObjectMapper mapper, MimeType... mimeTypes) {
+        super(mapper, mimeTypes);
+    }
+
+
+    /**
+     * Configure "streaming" media types for which flushing should be performed
+     * automatically vs at the end of the stream.
+     * <p>By default this is set to {@link MediaType#APPLICATION_STREAM_JSON}.
+     *
+     * @param mediaTypes one or more media types to add to the list
+     * @see HttpMessageEncoder#getStreamingMediaTypes()
+     */
+    public void setStreamingMediaTypes(List<MediaType> mediaTypes) {
+        this.streamingMediaTypes.clear();
+        this.streamingMediaTypes.addAll(mediaTypes);
+    }
+
+
+    @Override
+    public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) {
+        Class<?> clazz = elementType.toClass();
+        if (!supportsMimeType(mimeType)) {
+            return false;
+        }
+        if (mimeType != null && mimeType.getCharset() != null) {
+            Charset charset = mimeType.getCharset();
+            if (!ENCODINGS.containsKey(charset.name())) {
+                return false;
+            }
+        }
+        return (Object.class == clazz ||
+                (!String.class.isAssignableFrom(elementType.resolve(clazz)) && getObjectMapper().canSerialize(clazz)));
+    }
+
+    @Override
+    public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory,
+                                   ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
+
+        Assert.notNull(inputStream, "'inputStream' must not be null");
+        Assert.notNull(bufferFactory, "'bufferFactory' must not be null");
+        Assert.notNull(elementType, "'elementType' must not be null");
+
+        return LocaleUtils
+                .currentReactive()
+                .flatMapMany(locale -> {
+                    if (inputStream instanceof Mono) {
+                        return Mono.from(inputStream)
+                                   .map(value -> LocaleUtils
+                                           .doWith(value, locale,
+                                                   ((val, loc) ->
+                                                           encodeValue(val, bufferFactory, elementType, mimeType, hints)
+                                                   )
+                                           ))
+                                   .flux();
+                    } else {
+                        byte[] separator = streamSeparator(mimeType);
+                        if (separator != null) { // streaming
+                            try {
+                                ObjectWriter writer = createObjectWriter(elementType, mimeType, hints);
+                                ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer
+                                                                                            .getFactory()
+                                                                                            ._getBufferRecycler());
+                                JsonEncoding encoding = getJsonEncoding(mimeType);
+                                JsonGenerator generator = getObjectMapper()
+                                        .getFactory()
+                                        .createGenerator(byteBuilder, encoding);
+                                SequenceWriter sequenceWriter = writer.writeValues(generator);
+
+                                return Flux
+                                        .from(inputStream)
+                                        .map(value -> LocaleUtils
+                                                .doWith(value,
+                                                        locale,
+                                                        ((val, loc) -> this
+                                                                .encodeStreamingValue(val,
+                                                                                      bufferFactory,
+                                                                                      hints,
+                                                                                      sequenceWriter,
+                                                                                      byteBuilder,
+                                                                                      separator)
+                                                        )
+                                                ))
+                                        .doAfterTerminate(() -> {
+                                            try {
+                                                byteBuilder.release();
+                                                generator.close();
+                                            } catch (IOException ex) {
+                                                logger.error("Could not close Encoder resources", ex);
+                                            }
+                                        });
+                            } catch (IOException ex) {
+                                return Flux.error(ex);
+                            }
+                        } else { // non-streaming
+                            ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType);
+                            return Flux.from(inputStream)
+                                       .collectList()
+                                       .map(value -> LocaleUtils
+                                               .doWith(value, locale,
+                                                       ((val, loc) ->
+                                                               encodeValue(val, bufferFactory, listType, mimeType, hints)
+                                                       )
+                                               ))
+                                       .flux();
+                        }
+
+                    }
+                });
+    }
+
+    @Override
+    public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory,
+                                  ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
+
+        ObjectWriter writer = createObjectWriter(valueType, mimeType, hints);
+        ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.getFactory()._getBufferRecycler());
+        try {
+            JsonEncoding encoding = getJsonEncoding(mimeType);
+
+            logValue(hints, value);
+
+            try (JsonGenerator generator = getObjectMapper().getFactory().createGenerator(byteBuilder, encoding)) {
+                writer.writeValue(generator, value);
+                generator.flush();
+            } catch (InvalidDefinitionException ex) {
+                throw new CodecException("Type definition error: " + ex.getType(), ex);
+            } catch (JsonProcessingException ex) {
+                throw new EncodingException("JSON encoding error: " + ex.getOriginalMessage(), ex);
+            } catch (IOException ex) {
+                throw new IllegalStateException("Unexpected I/O error while writing to byte array builder", ex);
+            }
+
+            byte[] bytes = byteBuilder.toByteArray();
+            DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length);
+            buffer.write(bytes);
+
+            return buffer;
+        } finally {
+            byteBuilder.release();
+        }
+    }
+
+    private DataBuffer encodeStreamingValue(Object value, DataBufferFactory bufferFactory, @Nullable Map<String, Object> hints,
+                                            SequenceWriter sequenceWriter, ByteArrayBuilder byteArrayBuilder, byte[] separator) {
+
+        logValue(hints, value);
+
+        try {
+            sequenceWriter.write(value);
+            sequenceWriter.flush();
+        } catch (InvalidDefinitionException ex) {
+            throw new CodecException("Type definition error: " + ex.getType(), ex);
+        } catch (JsonProcessingException ex) {
+            throw new EncodingException("JSON encoding error: " + ex.getOriginalMessage(), ex);
+        } catch (IOException ex) {
+            throw new IllegalStateException("Unexpected I/O error while writing to byte array builder", ex);
+        }
+
+        byte[] bytes = byteArrayBuilder.toByteArray();
+        byteArrayBuilder.reset();
+
+        int offset;
+        int length;
+        if (bytes.length > 0 && bytes[0] == ' ') {
+            // SequenceWriter writes an unnecessary space in between values
+            offset = 1;
+            length = bytes.length - 1;
+        } else {
+            offset = 0;
+            length = bytes.length;
+        }
+        DataBuffer buffer = bufferFactory.allocateBuffer(length + separator.length);
+        buffer.write(bytes, offset, length);
+        buffer.write(separator);
+
+        return buffer;
+    }
+
+    private void logValue(@Nullable Map<String, Object> hints, Object value) {
+        if (!Hints.isLoggingSuppressed(hints)) {
+            LogFormatUtils.traceDebug(logger, traceOn -> {
+                String formatted = LogFormatUtils.formatValue(value, !traceOn);
+                return Hints.getLogPrefix(hints) + "Encoding [" + formatted + "]";
+            });
+        }
+    }
+
+    private ObjectWriter createObjectWriter(ResolvableType valueType, @Nullable MimeType mimeType,
+                                            @Nullable Map<String, Object> hints) {
+
+        JavaType javaType = getJavaType(valueType.getType(), null);
+        Class<?> jsonView = (hints != null ? (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null);
+        ObjectWriter writer = (jsonView != null ?
+                getObjectMapper().writerWithView(jsonView) : getObjectMapper().writer());
+
+        if (javaType.isContainerType()) {
+            writer = writer.forType(javaType);
+        }
+
+        return customizeWriter(writer, mimeType, valueType, hints);
+    }
+
+    protected ObjectWriter customizeWriter(ObjectWriter writer, @Nullable MimeType mimeType,
+                                           ResolvableType elementType, @Nullable Map<String, Object> hints) {
+
+        return writer;
+    }
+
+    @Nullable
+    private byte[] streamSeparator(@Nullable MimeType mimeType) {
+        for (MediaType streamingMediaType : this.streamingMediaTypes) {
+            if (streamingMediaType.isCompatibleWith(mimeType)) {
+                return STREAM_SEPARATORS.getOrDefault(streamingMediaType, NEWLINE_SEPARATOR);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Determine the JSON encoding to use for the given mime type.
+     *
+     * @param mimeType the mime type as requested by the caller
+     * @return the JSON encoding to use (never {@code null})
+     * @since 5.0.5
+     */
+    protected JsonEncoding getJsonEncoding(@Nullable MimeType mimeType) {
+        if (mimeType != null && mimeType.getCharset() != null) {
+            Charset charset = mimeType.getCharset();
+            JsonEncoding result = ENCODINGS.get(charset.name());
+            if (result != null) {
+                return result;
+            }
+        }
+        return JsonEncoding.UTF8;
+    }
+
+
+    // HttpMessageEncoder
+
+    @Override
+    public List<MimeType> getEncodableMimeTypes() {
+        return getMimeTypes();
+    }
+
+    @Override
+    public List<MediaType> getStreamingMediaTypes() {
+        return Collections.unmodifiableList(this.streamingMediaTypes);
+    }
+
+    @Override
+    public Map<String, Object> getEncodeHints(@Nullable ResolvableType actualType, ResolvableType elementType,
+                                              @Nullable MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) {
+
+        return (actualType != null ? getHints(actualType) : Hints.none());
+    }
+
+
+    // Jackson2CodecSupport
+
+    @Override
+    protected <A extends Annotation> A getAnnotation(MethodParameter parameter, Class<A> annotType) {
+        return parameter.getMethodAnnotation(annotType);
+    }
+}

+ 88 - 0
hsweb-starter/src/test/java/org/hswebframework/web/starter/jackson/CustomJackson2jsonEncoderTest.java

@@ -0,0 +1,88 @@
+package org.hswebframework.web.starter.jackson;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.hswebframework.web.dict.EnumDict;
+import org.hswebframework.web.i18n.MessageSourceInitializer;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.context.i18n.LocaleContext;
+import org.springframework.context.i18n.SimpleLocaleContext;
+import org.springframework.context.support.ResourceBundleMessageSource;
+import org.springframework.core.ResolvableType;
+import org.springframework.core.io.buffer.DataBufferUtils;
+import org.springframework.core.io.buffer.DefaultDataBufferFactory;
+import org.springframework.http.MediaType;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.function.Predicate;
+
+public class CustomJackson2jsonEncoderTest {
+
+
+    @Before
+    public void init(){
+        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
+        messageSource.setDefaultEncoding("utf-8");
+        messageSource.setBasenames("i18n.messages");
+        MessageSourceInitializer.init(messageSource);
+    }
+
+    @Test
+    public void testI18n() {
+
+        doTest(new TestEntity(TestEnum.e1),Locale.forLanguageTag("en-US"),s->s.contains("Option1"));
+        doTest(new TestEntity(TestEnum.e1),Locale.forLanguageTag("zh-CN"),s->s.contains("选项1"));
+
+    }
+
+    public void doTest(TestEntity entity, Locale locale, Predicate<String> verify){
+
+        CustomJackson2jsonEncoder encoder = new CustomJackson2jsonEncoder(new ObjectMapper());
+
+        encoder.encode(Mono.just(entity),
+                       new DefaultDataBufferFactory(),
+                       ResolvableType.forType(TestEntity.class),
+                       MediaType.APPLICATION_JSON,
+                       Collections.emptyMap())
+               .as(DataBufferUtils::join)
+               .map(buf -> buf.toString(StandardCharsets.UTF_8))
+               .doOnNext(System.out::println)
+               .subscriberContext(ctx->ctx.put(LocaleContext.class,new SimpleLocaleContext(locale)))
+               .as(StepVerifier::create)
+               .expectNextMatches(verify)
+               .verifyComplete();
+    }
+
+    @Getter
+    @Setter
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class TestEntity {
+
+        private TestEnum testEnum;
+    }
+
+
+    @Getter
+    @AllArgsConstructor
+    public enum TestEnum implements EnumDict<String> {
+        e1("enum.e1"),
+        e2("enum.e2");
+
+        private final String text;
+
+        @Override
+        public String getValue() {
+            return name();
+        }
+
+    }
+}

+ 2 - 0
hsweb-starter/src/test/resources/i18n/messages_en_US.properties

@@ -0,0 +1,2 @@
+enum.e1=Option1
+enum.e2=Option2

+ 2 - 0
hsweb-starter/src/test/resources/i18n/messages_zh_CN.properties

@@ -0,0 +1,2 @@
+enum.e1=选项1
+enum.e2=选项2