3

私の本当の質問/問題にすぐに行きますが、HttpMessageConverter内のコントローラーのハンドラーメソッドのアノテーションにアクセスする方法はありますか?答えはノーだと確信しています(Springのソースコードを調べた後)。

MappingJacksonHttpMessageConverterを使用するときにペアになっているJacksonMixinsを使用する他の方法はありますか?私はすでにMappingJacksonHttpMessageConverterに基づいて独自のHttpMessageConverterを実装し、Jackson2.0を使用するように「アップグレード」しています。

Controller.class

@Controller
public class Controller {

    @JsonFilter({ @JsonMixin(target=MyTargetObject.class, mixin=MyTargetMixin.class) })
    @RequestMapping(value="/my-rest/{id}/my-obj", method=RequestMethod.GET, produces="application/json")
    public @ResponseBody List<MyTargetObject> getListOfFoo(@PathVariable("id") Integer id) {
        return MyServiceImpl.getInstance().getBarObj(id).getFoos();
    }
}

@JsonFilterマッパーに渡したいカスタムアノテーションです。マッパーは、ObjectMapperに直接自動的にフィードできます。

MappingJacksonHttpMessageConverter.class

public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConverter<Object> {

    ...

    @Override
    protected void writeInternal(Object object, HttpOutputMessage outputMessage) {

            //Obviously, no access to the HandlerMethod here.

    }

    ...
}

私はこの答えを広範囲に検索しました。これまでのところ、コントローラーの処理メソッド内でオブジェクトをJSONにシリアル化する人しか見ていません(すべてのメソッドでDRYの原則に繰り返し違反しています)。または、データオブジェクトに直接注釈を付けます(オブジェクトの公開方法に関するデカップリングや複数の構成はありません)。

HttpMessageConverterでは実行できない可能性があります。他に選択肢はありますか?インターセプターはHandlerMethodへのアクセスを許可しますが、ハンドラーメソッドの返されたオブジェクトへのアクセスは許可しません。

4

3 に答える 3

3

これは理想的なソリューションではありません。私の2番目の答えを見てください。

を使用してこれを解決しましたModelAndViewResolverAnnotationMethodHandlerAdapterデフォルトの処理が発生する前に、これらが常に最初に開始されることを知っているという特典を使用して、これらを直接登録することができます。したがって、Spring のドキュメント -

/**
 * Set a custom ModelAndViewResolvers to use for special method return types.
 * <p>Such a custom ModelAndViewResolver will kick in first, having a chance to resolve
 * a return value before the standard ModelAndView handling kicks in.
 */
public void setCustomModelAndViewResolver(ModelAndViewResolver customModelAndViewResolver) {
    this.customModelAndViewResolvers = new ModelAndViewResolver[] {customModelAndViewResolver};
}

インターフェイスを見ると、ModelAndViewResolver一部の機能をハンドラー メソッドの動作に拡張するために必要なすべての引数が含まれていることがわかりました。

public interface ModelAndViewResolver {

    ModelAndView UNRESOLVED = new ModelAndView();

    ModelAndView resolveModelAndView(Method handlerMethod,
            Class handlerType,
            Object returnValue,
            ExtendedModelMap implicitModel,
            NativeWebRequest webRequest);
}

のおいしい議論をすべて見てくださいresolveModelAndView! 私は事実上、Spring がリクエストについて知っているすべてのものにアクセスできます。MappingJacksonHttpMessageConverter以下は、単方向 (外向き) で except と非常によく似た動作をするようにインターフェイスを実装した方法です。

public class JsonModelAndViewResolver implements ModelAndViewResolver {

    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    public static final MediaType DEFAULT_MEDIA_TYPE = new MediaType("application", "json", DEFAULT_CHARSET);

    private boolean prefixJson = false;

    public void setPrefixJson(boolean prefixJson) {
        this.prefixJson = prefixJson;
    }

    /**
     * Converts Json.mixins() to a Map<Class, Class>
     *
     * @param jsonFilter Json annotation
     * @return Map of Target -> Mixin classes
     */
    protected Map<Class<?>, Class<?>> getMixins(Json jsonFilter) {

        Map<Class<?>, Class<?>> mixins = new HashMap<Class<?>, Class<?>>();

        if(jsonFilter != null) {
            for(JsonMixin jsonMixin : jsonFilter.mixins()) {
                mixins.put(jsonMixin.target(), jsonMixin.mixin());
            }
        }

        return mixins;
    }

    @Override
    public ModelAndView resolveModelAndView(Method handlerMethod, Class handlerType, Object returnValue, ExtendedModelMap implicitModel, NativeWebRequest webRequest) {

        if(handlerMethod.getAnnotation(Json.class) != null) {

            try {

                HttpServletResponse httpResponse = webRequest.getNativeResponse(HttpServletResponse.class);

                httpResponse.setContentType(DEFAULT_MEDIA_TYPE.toString());

                OutputStream out = httpResponse.getOutputStream();

                ObjectMapper objectMapper = new ObjectMapper();

                objectMapper.setMixInAnnotations(getMixins(handlerMethod.getAnnotation(Json.class)));

                JsonGenerator jsonGenerator =
                        objectMapper.getJsonFactory().createJsonGenerator(out, JsonEncoding.UTF8);

                if (this.prefixJson) {
                    jsonGenerator.writeRaw("{} && ");
                }

                objectMapper.writeValue(jsonGenerator, returnValue);

                out.flush();
                out.close();

                return null;

            } catch (JsonProcessingException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return UNRESOLVED;
    }

}

上記で使用した唯一のカスタム クラスは、@Jsonという 1 つのパラメーターを含む注釈クラスmixinsです。コントローラー側でこれを実装する方法は次のとおりです。

@Controller
public class Controller {

    @Json({ @JsonMixin(target=MyTargetObject.class, mixin=MyTargetMixin.class) })
    @RequestMapping(value="/my-rest/{id}/my-obj", method=RequestMethod.GET)
    public @ResponseBody List<MyTargetObject> getListOfFoo(@PathVariable("id") Integer id) {
        return MyServiceImpl.getInstance().getBarObj(id).getFoos();
    }
}

それはかなり素晴らしい単純さです。ModelAndViewResolver は、返されたオブジェクトを自動的に JSON に変換し、注釈付きのミックスインも適用します。

新しい 3.0 タグでは ModelAndViewResolver を直接構成できないため、Spring 2.5 の構成方法に戻す必要があります。多分彼らはこれを見落としたのでしょうか?

My Old Config (Spring 3.1 スタイルを使用)

<mvc:annotation-driven />

My New Config (Spring 2.5 スタイルを使用)

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> 
    <property name="customModelAndViewResolvers">
        <list>
            <bean class="my.package.mvc.JsonModelAndViewResolver" />
        </list>
    </property>
</bean>

^^ 3.0+ には、カスタム ModelAndViewResolver を接続する方法がありません。したがって、古いスタイルに戻ります。

カスタム注釈は次のとおりです。

ジェイソン

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Json {

    /**
     * A list of Jackson Mixins.
     * <p>
     * {@link http://wiki.fasterxml.com/JacksonMixInAnnotations}
     */
    JsonMixin[] mixins() default {};

}

JsonMixin

public @interface JsonMixin {
    public Class<? extends Serializable> target();
    public Class<?> mixin();
}
于 2012-08-08T18:33:02.590 に答える
2

以下に回答を投稿した後、これを行う方法を変更しました。HandlerMethodReturnValueHandlerを使用しました。カスタムの戻り値ハンドラーが最後にトリガーされるため、順序をオーバーライドするためにプログラムによる Web 構成を作成する必要がありました。デフォルトの前にそれらをトリガーする必要がありました。

@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
   ...
}

うまくいけば、これが誰かを以下の私の答えよりも良い方向に導くでしょう.

これにより、任意のオブジェクトを直接 JSON にシリアル化できました。@RequestMapping の has Produce="application/json" では、戻り値を常に JSON にシリアル化していました。

を使用したことを除いて、パラメーターバインディングについても同じことを行いましたHandlerMethodArgumentResolver。選択した注釈でクラスに注釈を付けるだけです (通常はモデルにシリアル化するため、JPA @Entity を使用しました)。

ボイラープラター コードを必要とせずに、Spring コントローラーで POJO から JSON へのシームレスなデシリアライゼーションができるようになりました。

ボーナス: 私が持っている引数リゾルバーは、パラメーターの @Id タグをチェックします。JSON に Id のキーが含まれている場合、エンティティが取得され、JSON が永続化されたオブジェクトに適用されます。バム。

/**
 * De-serializes JSON to a Java Object.
 * <p>
 * Also provides handling of simple data type validation.  If a {@link JsonMappingException} is thrown then it
 * is wrapped as a {@link ValidationException} and handled by the MVC/validation framework.
 *
 * @author John Strickler
 * @since 2012-08-28
 */
public class EntityArgumentResolver implements HandlerMethodArgumentResolver {

    @Autowired
    private SessionFactory sessionFactory;

    private final ObjectMapper objectMapper = new ObjectMapper();

    private static final Logger log = Logger.getLogger(EntityArgumentResolver.class);

    //whether to log the incoming JSON
    private boolean doLog = false;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().getAnnotation(Entity.class) != null;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        String requestBody = IOUtils.toString(request.getReader());
        Class<?> targetClass = parameter.getParameterType();
        Object entity = this.parse(requestBody, targetClass);
        Object entityId = getId(entity);

        if(doLog) {
            log.info(requestBody);
        }

        if(entityId != null) {
            return copyObjectToPersistedEntity(entity, getKeyValueMap(requestBody), entityId);
        } else {
            return entity;
        }
    }


    /**
     * @param rawJson a json-encoded string
     * @return a {@link Map} consisting of the key/value pairs of the JSON-encoded string
     */
    @SuppressWarnings("unchecked")
    private Map<String, Object> getKeyValueMap(String rawJson) throws JsonParseException, JsonMappingException, IOException {
        return objectMapper.readValue(rawJson, HashMap.class);
    }


    /**
     * Retrieve an existing entity and copy the new changes onto the entity.
     *
     * @param changes a recently deserialized entity object that contains the new changes
     * @param rawJson the raw json string, used to determine which keys were passed to prevent
     *                copying unset/null values over to the persisted entity
     * @return the persisted entity with the new changes copied onto it
     * @throws NoSuchMethodException
     * @throws SecurityException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     * @throws IllegalArgumentException
     */
    private Object copyObjectToPersistedEntity(Object changesObject, Map<String, Object> changesMap, Object id) throws SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {

        Session session = sessionFactory.openSession();

        Object persistedObject =
                session.get(changesObject.getClass(), (Serializable) id);

        session.close();

        if(persistedObject == null) {
            throw new ValidationException(changesObject.getClass().getSimpleName() + " #" + id + " not found.");
        }

        Class<?> clazz = persistedObject.getClass();

        for(Method getterMethod : ReflectionUtils.getAllDeclaredMethods(clazz)) {

            Column column = getterMethod.getAnnotation(Column.class);

            //Column annotation is required
            if(column == null) {
                continue;
            }

            //Is the field allowed to be updated?
            if(!column.updatable()) {
                continue;
            }

            //Was this change a part of JSON request body?
            //(prevent fields false positive copies when certain fields weren't included in the JSON body)
            if(!changesMap.containsKey(BeanUtils.toFieldName(getterMethod))) {
                continue;
            }

            //Is the new field value different from the existing/persisted field value?
            if(ObjectUtils.equals(getterMethod.invoke(persistedObject), getterMethod.invoke(changesObject))) {
                continue;
            }

            //Copy the new field value to the persisted object
            log.info("Update " + clazz.getSimpleName() + "(" + id + ") [" + column.name() + "]");

            Object obj = getterMethod.invoke(changesObject);

            Method setter = BeanUtils.toSetter(getterMethod);

            setter.invoke(persistedObject, obj);

        }

        return persistedObject;
    }


    /**
     * Check if the recently deserialized entity object was populated with its ID field
     *
     * @param entity the object
     * @return an object value if the id exists, null if no id has been set
     */
    private Object getId(Object entity) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {

        for(Method method : ReflectionUtils.getAllDeclaredMethods(entity.getClass())) {
            if(method.getAnnotation(Id.class) != null) {
                method.setAccessible(true);
                return method.invoke(entity);
            }
        }

        return null;
    }


    private <T> T parse(String json, Class<T> clazz) throws JsonParseException, IOException {
        try {
            return objectMapper.readValue(json, clazz);
        } catch(JsonMappingException e) {
            throw new ValidationException(e);
        }
    }

    public void setDoLog(boolean doLog) {
        this.doLog = doLog;
    }

}
于 2013-01-23T16:19:42.693 に答える