Как написать аспект для обслуживания различных объектов и аргументов метода - PullRequest
1 голос
/ 20 сентября 2019

Я ищу написать Aspect, который можно использовать для макетирования данных в зависимости от некоторого значения, переданного в методе.Этот макет заменит фактический вызов REST.Если значение не совпадает, вернитесь к фактическому методу и сделайте вызов к конечной точке REST.

У меня уже есть написанный аспект, который работает в производстве, но очень грязный и должен был реализовать несколько различных методовчтобы удовлетворить потребность возвращать разные объекты и обрабатывать аргументы разных методов.Есть ли способ, которым я мог бы обобщить это так, чтобы у меня был как единый метод для выполнения работы и возможность использовать, если в будущих случаях мне не нужно беспокоиться о типах объектов и сигнатурах методов.

Текущийреализация осуществляется следующим образом.Как я мог бы изменить это, чтобы работать как единый метод, который может обслуживать будущие методы и типы ответов без необходимости изменять мой аспект?

Чтобы уточнить, у меня есть следующие 3 метода, которые я хочу смоделировать с помощью Aspect.как вы видите, у меня есть пользовательская аннотация, которая принимает имя.Ни один из аргументов метода не совпадает.

@MyMock(name = "ABC")
public AResponse getAResponse(ARequest aRequest){
    // make a REST call
}

@MyMock(name = "DEF")
public BResponse getBResponse(String fruit, BRequest bRequest){
    // make a REST call
}

@MyMock(name = "GHJ")
public CResponse getCResponse(String vehicle, CRequest cRequest, int id){
    // make a REST call
}

Класс аспектов в настоящее время выглядит следующим образом.Я использую значение имени в аннотации, чтобы определить, какой метод вызывать и какой тип значения возвращать.Как видите, это не очень масштабируемо.Мне нужно писать новый метод и логику каждый раз, когда я внедряю новые макеты.

@Around("@annotation(myMock)")
public Object getMockedData(ProceedingJoinPoint pjp, MyMock myMock) throws Throwable {
    String servName = performMocking.name();
    Object[] methodArguments = pjp.getArgs();
    MethodSignature signature = (MethodSignature) pjp.getSignature();
    Class returnType = signature.getReturnType();

    switch (myMock.name()) {
        case "ABC":
        getA(methodArguments[0]);
        break;
        case "DEF":
        getB(methodArguments[0]);
        break;
        case "GHJ":
        getC(methodArguments[2]);
        break;
    }
    return pjp.proceed(methodArguments);
}

private AResponse getA(ARequest aRequest){
     // I use methodArguments[0] (which is aRequest) to decide what value to return as a mock response here.
     // There is a getName value in that object which I use to reference  
}

private BResponse getB(BRequest bRequest){
     // I use methodArguments[0] (which is a String) to decide what value to return as a mock response here. 
}

private CResponse getC(CRequest cRequest){
     // I use methodArguments[2] (which is an int) to decide what value to return as a mock response here. 
}

Все вышеперечисленные методы get вызывают внешний JSON-файл для извлечения фиктивных данных.Содержание файлов выглядит следующим образом.Если ключи совпадают, будет дан ложный ответ, иначе вернитесь к фактическому вызову REST.

{
    "ABC": {
        "enabled": "true",
        "responses": [
            {
                "a_name": "{some valid json response matching the AResponse Object structure}"
            }
        ]
    },
    "DEF": {
        "enabled": "true",
        "responses": [
            {
                "d_name": "{some valid json response matching the BResponse Object structure}"
            }
        ]
    },
    "GHJ": {
        "enabled": "true",
        "responses": [
            {
                "123": "{some valid json response matching the CResponse Object structure}"
            }
        ]
    }
}

РЕДАКТИРОВАТЬ: добавлен мой класс ASPECT следующим образом:

package com.company.a.b.mock;

import com.domain.abc.b.logging.util.MyLogger;
import com.domain.abc.a.util.generic.ConfigHelper;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.List;
import java.util.Map;

@Aspect
@Component
public class AspectMocking {

    private final ConfigHelper configHelper;
    private final MyLogger myLogger;

    public AspectMocking(ConfigHelper configHelper, MyLogger myLogger) {
        this.configHelper = configHelper;
        this.myLogger = myLogger;
    }

    @Around("@annotation(myMock)")
    public Object getMockedData(ProceedingJoinPoint pjp, MyMock myMock) throws Throwable {

        final String env = System.getProperty("spring.profiles.active");
        String response = null;
        Object returnObject = null;
        String logMessage = null;
        String servName = myMock.name();
        Object[] methodArguments = pjp.getArgs();
        try {
            if ("test_env1".equals(env) || "test_env2".equals(env)) {

                MethodSignature signature = (MethodSignature) pjp.getSignature();
                Class returnType = signature.getReturnType();

                Map allDataFromMockedFile = getMockedDataFromFile();
                Map getResultForKey = (Map) allDataFromMockedFile.get(myMock.name());

                List result = null;
                if(getResultForKey != null){
                    result  = (List) getResultForKey.get("responses");
                }

                switch (myMock.name()) {
                case "ABC":
                    response = fetchABCMockedData(result, (String) methodArguments[0]);
                    logMessage = "Fetching ABC mock data for ABC Response: " + response;
                    break;
                case "V2_ABC":
                    response = fetchABCMockedData(getIntlResult(myMock.name(), (String)methodArguments[2]), (String) methodArguments[0]);
                    logMessage = "Fetching V2 ABC mock data for V2 ABC Response: " + response;
                    break;
                case "DEF":
                    response = fetchDEFMockedData(result, (String) methodArguments[0]);
                    logMessage = "Fetching DEF mock data: " + response;
                    break;
                case "GHJ":
                    response = fetchGHJMockedOfferData(result, (String) methodArguments[0], (String) methodArguments[1], (String) methodArguments[2]);
                    logMessage = "Fetching GHJ mock data: " + response;
                    break;
                }

                ObjectMapper mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
                returnObject = mapper.readValue(response, returnType);
            }
        } catch (Exception exp) {
            myLogger.addMessageAsWarning(String
                    .format("Exception occured for service %s while loading the mocked json, so hitting the actual service:"
                            + exp.getMessage(), servName));
        }

        if (returnObject == null) {
            returnObject = pjp.proceed(methodArguments);
        }

        myLogger.addMessage(logMessage);
        return returnObject;
    }


    private List getIntlResult(String name, String locale){
        Map localBasedMockFile = getMockedDataFromFile(locale.toLowerCase());
        Map localeSpecificMockedData = (Map) localBasedMockFile.get(name);
        return (List) localeSpecificMockedData.get("responses");
    }

    // had to add this recently as we needed locale values now to go to a diff file. 
    private Map getMockedDataFromFile(String countryCode){
        final String DATA_URL = String.format("/a/b/c/%s/data.json", countryCode);
        return configHelper.getMAVDataAsObject(DATA_URL, Map.class);
    }

    private Map getMockedDataFromFile() {
        final String DATA_URL = "/a/b/c/zh/mock.json";
        return configHelper.getMAVDataAsObject(DATA_URL, Map.class);
    }

    private String fetchABCMockedData(List<Map> allResponses, String vinNumber) throws IOException {
        String response = null;
        for (Map m : allResponses) {
            String mockedVinNumber = m.keySet().toString().replaceAll("[\\[\\]]", "");
            if (vinNumber.equals(mockedVinNumber)) {
                response = (String) m.get(mockedVinNumber);
            }
        }
        return response;
    }

    private String fetchDEFMockedData(List<String> allResponses, String vinNumber) {
        return allResponses.contains(vinNumber) ? "true" : "false";
    }

    private String fetchGHJMockedOfferData(List<Map> allResponses, String journey, String name, String pin) {
        String response = null;
        String key = journey+"_"+name + "_" + pin;

        for (Map m : allResponses) {
            String mockedKey = m.keySet().toString().replaceAll("[\\[\\]]", "");
            if (mockedKey.equals(key)) {
                response = (String) m.get(mockedKey);
            }
        }

        return response;
    }
}

1 Ответ

1 голос
/ 20 сентября 2019

Давайте сначала скомпилируем ваш пример кода, а затем продолжим разговор, хорошо?Вот мои MCVE :

Вспомогательные классы:

package de.scrum_master.app;

public class ARequest {}
package de.scrum_master.app;

public class BRequest {}
package de.scrum_master.app;

public class CRequest {}
package de.scrum_master.app;

public class AResponse {
  private String content;

  public AResponse(String content) {
    this.content = content;
  }

  @Override
  public String toString() {
    return "AResponse[content=" + content + "]";
  }
}
package de.scrum_master.app;

public class BResponse {
  private String content;

  public BResponse(String content) {
    this.content = content;
  }

  @Override
  public String toString() {
    return "BResponse[content=" + content + "]";
  }
}
package de.scrum_master.app;

public class CResponse {
  private String content;

  public CResponse(String content) {
    this.content = content;
  }

  @Override
  public String toString() {
    return "CResponse[content=" + content + "]";
  }
}

Маркерная аннотация:

package de.scrum_master.app;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RUNTIME)
@Target(METHOD)
public @interface MyMock {
  String name();
}

Приложение драйвера:

package de.scrum_master.app;

public class Application {
  @MyMock(name = "ABC")
  public AResponse getAResponse(ARequest aRequest) {
    // make a REST call
    return new AResponse("real A response");
  }

  @MyMock(name = "DEF")
  public BResponse getBResponse(String fruit, BRequest bRequest) {
    // make a REST call
    return new BResponse("real B response");
  }

  @MyMock(name = "GHJ")
  public CResponse getCResponse(String vehicle, CRequest cRequest, int id) {
    // make a REST call
    return new CResponse("real C response");
  }

  public static void main(String[] args) {
    Application application = new Application();
    System.out.println(application.getAResponse(new ARequest()));
    System.out.println(application.getBResponse("apple", new BRequest()));
    System.out.println(application.getCResponse("bicycle", new CRequest(), 11));
  }
}

Журнал консоли без аспекта:

AResponse[content=real A response]
BResponse[content=real B response]
CResponse[content=real C response]

Аспект, вариант A, с использованием аннотации маркера:

package de.scrum_master.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

import de.scrum_master.app.ARequest;
import de.scrum_master.app.AResponse;
import de.scrum_master.app.BResponse;
import de.scrum_master.app.CResponse;
import de.scrum_master.app.MyMock;

@Aspect
public class MockResponseAspect {
  @Around("@annotation(myMock)")
  public Object getMockedData(ProceedingJoinPoint pjp, MyMock myMock) throws Throwable {
    // Whatever this does...
    //String servName = performMocking.name();
    Object[] methodArguments = pjp.getArgs();

    switch (myMock.name()) {
      case "ABC": return getA((ARequest) methodArguments[0]);
      case "DEF": return getB((String) methodArguments[0]);
      case "GHJ": return getC((int) methodArguments[2]);
      default: return pjp.proceed(methodArguments); 
    }
  }

  private AResponse getA(ARequest aRequest) {
    return new AResponse("mock A response");
  }

  private BResponse getB(String fruit) {
    return new BResponse("mock B response");
  }

  private CResponse getC(int id) {
    return new CResponse("mock C response");
  }
}

Журнал консоли с использованием аспекта:

AResponse[content=mock A response]
BResponse[content=mock B response]
CResponse[content=mock C response]

Аспект, вариант B, с использованием типа возврата:

Это упрощенная версия аспекта, вообще не использующая фиктивную аннотацию.Вместо этого он мог бы использовать имя метода, как в моем примере, или, возможно, другую существующую аннотацию метода, такую ​​как @Response или что-то еще, что объединяет все методы ответа.

package de.scrum_master.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;

import de.scrum_master.app.ARequest;
import de.scrum_master.app.AResponse;
import de.scrum_master.app.BResponse;
import de.scrum_master.app.CResponse;

@Aspect
public class MockResponseAspect {
  @Around("execution(* get*Response(..))")
  public Object getMockedData(ProceedingJoinPoint pjp) throws Throwable {
    // Whatever this does...
    //String servName = performMocking.name();
    Object[] methodArguments = pjp.getArgs();
    MethodSignature signature = (MethodSignature) pjp.getSignature();
    Class<?> returnType = signature.getReturnType();

    switch (returnType.getSimpleName()) {
      case "AResponse": return getA((ARequest) methodArguments[0]);
      case "BResponse": return getB((String) methodArguments[0]);
      case "CResponse": return getC((int) methodArguments[2]);
      default: return pjp.proceed(methodArguments); 
    }
  }

  private AResponse getA(ARequest aRequest) {
    return new AResponse("mock A response");
  }

  private BResponse getB(String fruit) {
    return new BResponse("mock B response");
  }

  private CResponse getC(int id) {
    return new CResponse("mock C response");
  }
}

Журнал консоли такой же, как и для первогоаспект.

Теперь нам есть что обсудить.Что тебе не нравится в этом?Чего ты хочешь достичь?

...