问题
In my spring rest app, I log every api endpoints arguments in aspect.
@Aspect
@Component
public class EndpointsAspect {
@Around("execution(@org.springframework.web.bind.annotation.RequestMapping * *(..))")
public Object handle(ProceedingJoinPoint joinPoint) throws Throwable {
Map<String, Object> log = new HashMap<>();
String[] parameterNames = methodSignature.getParameterNames();
Object[] parameterValues = joinPoint.getArgs();
Map<String, Object> arguments = new HashMap<>();
for (int i = 0; i < parameterNames.length; i++) {
arguments.put(parameterNames[i], parameterValues[i]);
}
log.put("Method arguments", arguments);
Gson gson = new GsonBuilder().setPrettyPrinting().create();
String json = gson.toJson(log);
...
Object retVal = joinPoint.proceed();
}
}
It works fine, until one of argument of adviced method has argument of type HttpServletRequest
@RequestMapping("/info")
public String index(HttpServletRequest request) {
return "Info";
}
In this case java.lang.StackOverflowError is raised.
I know that this is somehow related to HttpServlterRequest variable (maybe some infinitive loop), but how to solve this problem?
Ho to limit gson depth?
I've looked at some solutions (annotate fields or classes that should be converted to json with some annotations), but it's not suitable for me, this should be universal solutions for all classes and cases (I acn't, for instance, annotate HttpServletRequest with some annotations, or include it to gson exclusion strategy, because who nows wich classes will be converted to json), I need log data as json, but logger shouldn't be application fault point because of serialization issue.
Thank you.
回答1:
I've found some solution and answer my question.
For json serializer I use flexjson library (http://flexjson.sourceforge.net/). It supports serialization of classes which have bidirectional relationship without any annotations and other extra work.
In case of HttpServletRequest, I wrap serialization in try catch block and log that, for example, argument "request" could not be serialized.
@Around("execution(@org.springframework.web.bind.annotation.RequestMapping * *(..))")
public Object handle(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
Map<String, Object> log = new HashMap<>();
log.put("Method", joinPoint.getSignature().toString());
String[] parameterNames = methodSignature.getParameterNames();
Object[] parameterValues = joinPoint.getArgs();
Map<String, Object> arguments = new HashMap<>();
for (int i = 0; i < parameterNames.length; i++) {
// Check if argument can be serialized and put arguments to argument list if possible. For example, HttpServletRequest cannot be serialized
try {
JSONSerializer serializer = new JSONSerializer();
String json = serializer.prettyPrint(true).deepSerialize(parameterValues[i]);
arguments.put(parameterNames[i], parameterValues[i]);
}
catch (Exception serializerException) {
arguments.put(parameterNames[i], "Couldn't serialize argument. "+serializerException.getMessage());
}
}
log.put("Method arguments", arguments);
...
try {
JSONSerializer serializer = new JSONSerializer();
String json = serializer.prettyPrint(true).deepSerialize(log);
logger.info(json);
}
catch (Exception e) {
logger.error("Could not serialize data. "+e.getMessage());
}
...
}
And log looks something like this:
{
"Path": "/v1/users/1",
"Http Status": "200 OK",
"Method arguments": {
"request": "Couldn't serialize argument. Error trying to deepSerialize",
"id": 1
},
"Headers": {
"accept-language": "en-US,en;q=0.8",
"cookie": "lbannounce=; ServiceTransactionSuccess=false; ServiceTransactionAmount=false; ServiceTransactionWidgetType=false; ServiceTransactionDomain=false; _ga=GA1.1.1011235846.1448355706",
"host": "localhost:8080",
"connection": "keep-alive",
"accept-encoding": "gzip, deflate, sdch",
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"user-agent": "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.124 Safari/537.36"
},
"Return Value": {
"class": "api.domain.User",
"email": "user@mail.com",
"id": 1,
"username": "user111"
},
"Ip": "0:0:0:0:0:0:0:1",
"Http Method": "GET",
"Method": "User api.web.UserController.user(int,HttpServletRequest)"
}
回答2:
I don't know how important is for you to go deeper en each parameter value for logging, but one workaround could be to add the string representation for each parameter instead of the object:
for (int i = 0; i < parameterNames.length; i++) {
arguments.put(parameterNames[i], parameterValues[i].toString());
}
In case of HttpServletRequest you probably get an awful log, but it's a log! for your own classes you can overwrite toString() method to define it as you want (not a bad practice by the way).
If it doesn't work for you, you can add a hack for external classes (not inside you package root) for example:
for (int i = 0; i < parameterNames.length; i++) {
Object value = parameterValues[i];
if (!"com.mypackage.foo.bar".equals(value.getClass().getPackage().getName()) {
value = value.toString();
}
arguments.put(parameterNames[i], value);
}
It's a hack but it could help maybe.
But if you don't like that easy solution, the pretty one could be to use Gson serializers (as expianed here: https://sites.google.com/site/gson/gson-user-guide#TOC-Using-Gson), and you are responsible to implement hot an HttpServletRequest will be serialized
GsonBuilder gson = new GsonBuilder();
gson.registerTypeAdapter(HttpServletRequest.class, new MyCustonSerializer());
...
class MyCustonSerializer implements JsonSerializer<HttpServletRequest> {
public JsonElement serialize(HttpServletRequest src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.toString());
}
}
EDIT: - add a full example -
public class GsonTest {
public static void main(String[] args) {
GsonBuilder gb = new GsonBuilder();
gb.registerTypeAdapter(Integer.class, new MyCustonHttpServletRequestSerializer());
//gb.registerTypeAdapter(Object.class, null);
Map<String, Object> map = new HashMap<String, Object>();
map.put("a", "String a");
map.put("b", new Integer(3));
map.put("c", new Person("John", "Doe"));
Gson gson = gb.setPrettyPrinting().create();
String json = gson.toJson(map);
System.out.println(json);
}
}
class MyCustonHttpServletRequestSerializer implements JsonSerializer<Integer> {
@Override
public JsonElement serialize(Integer src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive("Number " + src);
}
}
class Person {
private String name;
private String lastName;
public Person(String name, String lastName) {
this.name = name;
this.lastName = lastName;
}
}
and output will be
{
"b": "Number 3",
"c": {
"name": "John",
"lastName": "Doe"
},
"a": "String a"
}
来源:https://stackoverflow.com/questions/33900388/java-lang-stackoverflowerror-converting-httpservletrequest-to-json