Feign - define param value for each methods - feign

I need to write a client with multiple methods that require the apiKey as query string param. Is it possible to allow the client's user to pass the api key only to the method withApiKey, so I can avoid to request the apiKey as first parameter of each method?
public interface Client {
#RequestLine("GET /search/search?key={apiKey}&query={query}&limit={limit}&offset={offset}")
SearchResponse search(#Param("apiKey") String apiKey, #Param("query") String query, #Param("limit") Integer limit, #Param("offset") Integer offset);
#RequestLine("GET /product/attributes?key={apiKey}&products={products}")
List<Product> getProduct(#Param("apiKey") String apiKey, #Param("products") String products);
public class Builder {
private String basePath;
private String apiKey;
public Client build() {
return Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.client(new ApacheHttpClient())
.logger(new Slf4jLogger())
.logLevel(Logger.Level.FULL)
.target(Client.class, basePath);
}
public Builder withBasePath(String basePath) {
this.basePath = basePath;
return this;
}
public Builder withApiKey(String apiKey) {
this.apiKey = apiKey;
return this;
}
}
}

Depending on the setup request-interceptors might work: https://github.com/OpenFeign/feign#request-interceptors
Hopefully the example below will help.
You can swap the builder out for just the interface annotation and then move the configuration to a configuration class, if you are using spring it could be like:
#FeignClient(
name = "ClerkClient",
url = "${clery-client.url}", // instead of the withBasePath method
configuration = {ClerkClientConfiguration.class}
)
public interface Client {
Then the ClerkClientConfiguration class can define the required config beans including a ClerkClientInterceptor
public class ClerkClientConfiguration {
#Bean
public RequestInterceptor clerkClientInterceptor() {
return new ClerkClientInterceptor();
}
Then the interceptor can have a value picked up from the config and added to the queries (or header etc)
public class ClerkClientInterceptor implements RequestInterceptor {
#Value("${clerk-client.key}")
private String apiKey
#Override public void apply(RequestTemplate template) {
requestTemplate.query( "key", apiKey);
}

Related

Custom DynamoDb TableNameResolver not being called when using CrudRepository

I am testing DynamoDB tables and want to set up different table names for prod and dev environment using the keyword"dev" for development and prod for production.
I have a POJO
#DynamoDBTable(tableName = "abc_xy_dev_MyProjectName_Employee")
public class Employee implements Cloneable {
}
On Prod I want its name to be abc_xy_prod_MyProjectName_Employee.
So, I wrote a TableNameResolver
public static class MyTableNameResolver implements TableNameResolver {
public static final MyTableNameResolver INSTANCE = new MyTableNameResolver();
#Override
public String getTableName(Class<?> clazz, DynamoDBMapperConfig config) {
final TableNameOverride override = config.getTableNameOverride();
String tableNameToReturn = null;
if (override != null) {
final String tableName = override.getTableName();
if (tableName != null) {
System.out.println("MyTableNameResolver ==================================");
return tableName;
}
}
String env = System.getenv("DEPLOYMENT_ENV");
for(Annotation annotation : clazz.getAnnotations()){
if(annotation instanceof DynamoDBTable){
DynamoDBTable myAnnotation = (DynamoDBTable) annotation;
if ("production".equals(env)){
tableNameToReturn = myAnnotation.tableName().replace("_dev_", "_prod_");
}
else {
tableNameToReturn = myAnnotation.tableName();
}
}
}
return tableNameToReturn;
}
}
This works by creating a table with the name abc_xy_prod_MyProjectName_Employee in production.
However, I have a repository with the following code
#EnableScan
public interface EmployeeRepository extends CrudRepository<Employee, String>
{
#Override
<S extends Employee> S save(S employee);
Optional<Employee> findById(String id);
#Override
List<Employee> findAll();
Optional<Employee> findByEmployeeNumber(String EmployeeNumber);
}
Thus when i try to call the method findAll via a endpoint /test case, i get the exception
There was an unexpected error (type=Internal Server Error,
status=500). User:
arn:aws:iam::87668976786:user/svc_nac_ps_MyProjectName_prod is not
authorized to perform: dynamodb:Scan on resource:
:table/abc_xy_dev_MyProjectName_Employee (Service: AmazonDynamoDBv2;
Status Code: 400; Error Code: AccessDeniedException; Request ID:
aksdnhLDFL)
i.e MyTableNameResolver doesn't get called internally when the respository methods are executed. It still points to table name with the name abc_xy_dev_MyProjectName_Employee given in the annotation #DynamoDBTable(tableName = "abc_xy_dev_MyProjectName_Employee")
You have used spring JPA as persistence dynamoDB Integration.
Below configuration can be used to set table name override as part of spring boot configuration.
Sample example is found in https://github.com/ganesara/SpringExamples/tree/master/spring-dynamo
Map Dynamo db repository with user defined mapper config reference
#EnableDynamoDBRepositories(basePackages = "home.poc.spring", dynamoDBMapperConfigRef="dynamoDBMapperConfig")
Mapper Config for table override is as below
#Bean
public DynamoDBMapperConfig dynamoDBMapperConfig() {
DynamoDBMapperConfig mapperConfig = new DynamoDBMapperConfig
.Builder()
.withTableNameOverride(DynamoDBMapperConfig.TableNameOverride.withTableNamePrefix("PROD_"))
.build();
return mapperConfig;
}
Full configuration for reference
#Configuration
#EnableDynamoDBRepositories(basePackages = "home.poc.spring", dynamoDBMapperConfigRef="dynamoDBMapperConfig")
public class DynamoDBConfig {
#Value("${amazon.dynamodb.endpoint}")
private String amazonDynamoDBEndpoint;
#Value("${amazon.aws.accesskey}")
private String amazonAWSAccessKey;
#Value("${amazon.aws.secretkey}")
private String amazonAWSSecretKey;
#Bean
public AmazonDynamoDB amazonDynamoDB() {
AmazonDynamoDB amazonDynamoDB
= new AmazonDynamoDBClient(amazonAWSCredentials());
if (!StringUtils.isEmpty(amazonDynamoDBEndpoint)) {
amazonDynamoDB.setEndpoint(amazonDynamoDBEndpoint);
}
return amazonDynamoDB;
}
#Bean
public AWSCredentials amazonAWSCredentials() {
return new BasicAWSCredentials(
amazonAWSAccessKey, amazonAWSSecretKey);
}
#Bean
public DynamoDBMapperConfig dynamoDBMapperConfig() {
DynamoDBMapperConfig mapperConfig = new DynamoDBMapperConfig
.Builder()
.withTableNameOverride(DynamoDBMapperConfig.TableNameOverride.withTableNamePrefix("PROD_"))
.build();
return mapperConfig;
}
#Bean
public DynamoDBMapper dynamoDBMapper() {
return new DynamoDBMapper(amazonDynamoDB(), dynamoDBMapperConfig());
}
}
You are using DynamoDBMapper (the Java SDK). Here is how I use it. Lets say I have a table called Users, with an associated User POJO. In DynamoDB I have DEV_Users and LIVE_Users.
I have an environment variable 'ApplicationEnvironmentName' which is either DEV or LIVE.
I create a custom DynamoDBMapper like this:
public class ApplicationDynamoMapper {
private static Map<String, DynamoDBMapper> mappers = new HashMap<>();
private static AmazonDynamoDB client = AmazonDynamoDBClientBuilder.standard()
.withRegion(System.getProperty("DynamoDbRegion")).build();
protected ApplicationDynamoMapper() {
// Exists only to defeat instantiation.
}
public static DynamoDBMapper getInstance(final String tableName) {
final ApplicationLogContext LOG = new ApplicationLogContext();
DynamoDBMapper mapper = mappers.get(tableName);
if (mapper == null) {
final String tableNameOverride = System.getProperty("ApplicationEnvironmentName") + "_" + tableName;
LOG.debug("Creating DynamoDBMapper with overridden tablename {}.", tableNameOverride);
final DynamoDBMapperConfig mapperConfig = new DynamoDBMapperConfig.Builder().withTableNameOverride(TableNameOverride.withTableNameReplacement(tableNameOverride)).build();
mapper = new DynamoDBMapper(client, mapperConfig);
mappers.put(tableName, mapper);
}
return mapper;
}
}
My Users POJO looks like this:
#DynamoDBTable(tableName = "Users")
public class User {
...
}
When I want to use the database I create an application mapper like this:
DynamoDBMapper userMapper = ApplicationDynamoMapper.getInstance(User.DB_TABLE_NAME);
If I wanted to a load a User, I would do it like this:
User user = userMapper.load(User.class, userId);
Hope that helps.

delivery headers by ZuulFilter fail

I have overridden the run() method in a ZuulFilter as
RequestContext ctx = RequestContext.getCurrentContext();
ctx.addZuulRequestHeader('header name', 'value');
However, in another service I can't find the header from the request.
More details, filterType is "pre" and there is only one filter.
Header added in a Zuul filter,
context.addZuulRequestHeader("my-header", "my-value");
can be retrieved in another filter this way,
context.getZuulRequestHeaders().get("my-header");
I also try to keep my pre-filter order low, e.g., -1000;
You can retrieve the header in an internal service, a client of Zuul, as you would do it for any other header. There is no distinction between regular request header and Zuul header, e.g.,
#RequestMapping(value = /abc/hello, method = RequestMethod.GET)
public MyObject read(#RequestHeader("my-header") String value) {
...
}
Example filter adding a header:
public class AddHeaderFilter extends ZuulFilter {
public String filterType() {
return "pre";
}
public int filterOrder() {
return 100;
}
public boolean shouldFilter() {
return true;
}
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
context.addZuulRequestHeader('header name', "value");
return null;
}
}
Then in the application configuration you have to load the filter as a bean:
#Bean
AddHeaderFilter addHeaderFilter() {
return new AddHeaderFilter();
}

How can I change the feign URL during the runtime?

#FeignClient(name = "test", url="http://xxxx")
How can I change the feign URL (url="http://xxxx") during the runtime? because the URL can only be determined at run time.
You can add an unannotated URI parameter (that can potentially be determined at runtime) and that will be the base path that will be used for the request. E.g.:
#FeignClient(name = "dummy-name", url = "https://this-is-a-placeholder.com")
public interface MyClient {
#PostMapping(path = "/create")
UserDto createUser(URI baseUrl, #RequestBody UserDto userDto);
}
And then the usage will be:
#Autowired
private MyClient myClient;
...
URI determinedBasePathUri = URI.create("https://my-determined-host.com");
myClient.createUser(determinedBasePathUri, userDto);
This will send a POST request to https://my-determined-host.com/create (source).
Feign has a way to provide the dynamic URLs and endpoints at runtime.
The following steps have to be followed:
In the FeignClient interface we have to remove the URL parameter. We have to use #RequestLine annotation to mention the REST method (GET, PUT, POST, etc.):
#FeignClient(name="customerProfileAdapter")
public interface CustomerProfileAdaptor {
// #RequestMapping(method=RequestMethod.GET, value="/get_all")
#RequestLine("GET")
public List<Customer> getAllCustomers(URI baseUri);
// #RequestMapping(method=RequestMethod.POST, value="/add")
#RequestLine("POST")
public ResponseEntity<CustomerProfileResponse> addCustomer(URI baseUri, Customer customer);
#RequestLine("DELETE")
public ResponseEntity<CustomerProfileResponse> deleteCustomer(URI baseUri, String mobile);
}
In RestController you have to import FeignClientConfiguration
You have to write one RestController constructor with encoder and decoder as parameters.
You need to build the FeignClient with the encoder, decoder.
While calling the FeignClient methods, provide the URI (BaserUrl + endpoint) along with rest call parameters if any.
#RestController
#Import(FeignClientsConfiguration.class)
public class FeignDemoController {
CustomerProfileAdaptor customerProfileAdaptor;
#Autowired
public FeignDemoController(Decoder decoder, Encoder encoder) {
customerProfileAdaptor = Feign.builder().encoder(encoder).decoder(decoder)
.target(Target.EmptyTarget.create(CustomerProfileAdaptor.class));
}
#RequestMapping(value = "/get_all", method = RequestMethod.GET)
public List<Customer> getAllCustomers() throws URISyntaxException {
return customerProfileAdaptor
.getAllCustomers(new URI("http://localhost:8090/customer-profile/get_all"));
}
#RequestMapping(value = "/add", method = RequestMethod.POST)
public ResponseEntity<CustomerProfileResponse> addCustomer(#RequestBody Customer customer)
throws URISyntaxException {
return customerProfileAdaptor
.addCustomer(new URI("http://localhost:8090/customer-profile/add"), customer);
}
#RequestMapping(value = "/delete", method = RequestMethod.POST)
public ResponseEntity<CustomerProfileResponse> deleteCustomer(#RequestBody String mobile)
throws URISyntaxException {
return customerProfileAdaptor
.deleteCustomer(new URI("http://localhost:8090/customer-profile/delete"), mobile);
}
}
I don`t know if you use spring depend on multiple profile.
for example: like(dev,beta,prod and so on)
if your depend on different yml or properties. you can define FeignClientlike:(#FeignClient(url = "${feign.client.url.TestUrl}", configuration = FeignConf.class))
then
define
feign:
client:
url:
TestUrl: http://dev:dev
in your application-dev.yml
define
feign:
client:
url:
TestUrl: http://beta:beta
in your application-beta.yml (I prefer yml).
......
thanks god.enjoy.
use feign.Target.EmptyTarget
#Bean
public BotRemoteClient botRemoteClient(){
return Feign.builder().target(Target.EmptyTarget.create(BotRemoteClient.class));
}
public interface BotRemoteClient {
#RequestLine("POST /message")
#Headers("Content-Type: application/json")
BotMessageRs sendMessage(URI url, BotMessageRq message);
}
botRemoteClient.sendMessage(new URI("http://google.com"), rq)
You can create the client manually:
#Import(FeignClientsConfiguration.class)
class FooController {
private FooClient fooClient;
private FooClient adminClient;
#Autowired
public FooController(ResponseEntityDecoder decoder, SpringEncoder encoder, Client client) {
this.fooClient = Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
.target(FooClient.class, "http://PROD-SVC");
this.adminClient = Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
.target(FooClient.class, "http://PROD-SVC");
}
}
From documentation: https://cloud.spring.io/spring-cloud-netflix/multi/multi_spring-cloud-feign.html#_creating_feign_clients_manually
In interface you can change url by Spring annotations. The base URI is configured in yml Spring configuration.
#FeignClient(
name = "some.client",
url = "${some.serviceUrl:}",
configuration = FeignClientConfiguration.class
)
public interface SomeClient {
#GetMapping("/metadata/search")
String search(#RequestBody SearchCriteria criteria);
#GetMapping("/files/{id}")
StreamingResponseBody downloadFileById(#PathVariable("id") UUID id);
}
Use #PathVariable like this:
#Service
#FeignClient(name = "otherservicename", decode404 = true)
public interface myService {
#RequestMapping(method = RequestMethod.POST, value = "/basepath/{request-path}")
ResponseEntity<String> getResult(#RequestHeader("Authorization") String token,
#RequestBody HashMap<String, String> reqBody,
#PathVariable(value = "request-path") String requestPath);
}
Then from service, construct the dynamic url path and send the request:
String requestPath = "approve-req";
ResponseEntity<String> responseEntity = myService.getResult(
token, reqBody, requestPath);
Your request url will be at: "/basepath/approve-req"
I prefer to build feign client by configuration to pass a url at run time (in my case i get the url by service name from consul discovery service)
so i extend feign target class as below:
public class DynamicTarget<T> implements Target<T> {
private final CustomLoadBalancer loadBalancer;
private final String serviceId;
private final Class<T> type;
public DynamicTarget(String serviceId, Class<T> type, CustomLoadBalancer loadBalancer) {
this.loadBalancer = loadBalancer;
this.serviceId = serviceId;
this.type = type;
}
#Override
public Class<T> type() {
return type;
}
#Override
public String name() {
return serviceId;
}
#Override
public String url() {
return loadBalancer.getServiceUrl(name());
}
#Override
public Request apply(RequestTemplate requestTemplate) {
requestTemplate.target(url());
return requestTemplate.request();
}
}
var target = new DynamicTarget<>(Services.service_id, ExamsAdapter.class, loadBalancer);
package commxx;
import java.net.URI;
import java.net.URISyntaxException;
import feign.Client;
import feign.Feign;
import feign.RequestLine;
import feign.Retryer;
import feign.Target;
import feign.codec.Encoder;
import feign.codec.Encoder.Default;
import feign.codec.StringDecoder;
public class FeignTest {
public interface someItfs {
#RequestLine("GET")
String getx(URI baseUri);
}
public static void main(String[] args) throws URISyntaxException {
String url = "http://www.baidu.com/s?wd=ddd"; //ok..
someItfs someItfs1 = Feign.builder()
// .logger(new FeignInfoLogger()) // 自定义日志类,继承 feign.Logger
// .logLevel(Logger.Level.BASIC)// 日志级别
// Default(long period, long maxPeriod, int maxAttempts)
.client(new Client.Default(null, null))// 默认 http
.retryer(new Retryer.Default(5000, 5000, 1))// 5s超时,仅1次重试
// .encoder(Encoder)
// .decoder(new StringDecoder())
.target(Target.EmptyTarget.create(someItfs.class));
// String url = "http://localhost:9104/";
//
System.out.println(someItfs1.getx(new URI(url)));
}
}

Rest api how to get parameters?

I'm new to rest api.
I need to make an api that takes a string as parameter and then return boolean.
Now my question is how to i pass that string to my api, and then get the string inside my api?
Here's one example that takes a string in parameter and has a default value if the query parameter is not provided:
#Path("business/department/")
public interface DepartmentService {
#GET
#Path("/cs/availability/chat")
#Produces(MediaType.APPLICATION_JSON)
boolean getCustomerServiceAvailability(#QueryParam("type") #DefaultValue("chat") String type);
}
and the implementation class can be anything that implements your interface. In this example, it's a stateless EJB
#Stateless
public class DepartmentServiceImpl implements DepartmentService {
#Context
private HttpServletRequest request;
private static final Logger LOGGER = Logger.getLogger(DepartmentServiceImpl.class.getName());
#Override
public boolean getCustomerServiceAvailability(String scheduleType) {
RequestInfo reqInfo = new RequestInfo(request, this.getClass(), "getCustomerServiceAvailability");
boolean available;
try {
available = CallBusinessService(scheduleType);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, e.getLocalizedMessage());
throw new ServiceException();
} finally {
reqInfo.logExecutionTime();
}
}
}

Spring Java config message convertor priority

I have defined two convertors like this using Spring Java config. I always get a XML response unless I specified the 'Accept=applicaiton/json' in the HTTP header. Is there a way to set the default convertor to be JSON instead of XML convertor.
#EnableWebMvc
#Configuration
#ComponentScan(basePackages = {"foo.bar"})
public class WebMvcConfig extends WebMvcConfigurerAdapter {
...
#Bean
public MappingJackson2HttpMessageConverter jsonConverter() {
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
jsonConverter.setObjectMapper(objectMapper);
return jsonConverter;
}
#Bean
public MappingJackson2XmlHttpMessageConverter xmlConverter() {
MappingJackson2XmlHttpMessageConverter xmlConverter = new MappingJackson2XmlHttpMessageConverter();
return xmlConverter;
}
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(jsonConverter());
converters.add(xmlConverter());
super.configureMessageConverters(converters);
}
Here is my controller.
#RequestMapping(value = "/product")
public
#ResponseBody
BSONObject getProducts(#RequestParam String ids,
#RequestParam(required = false) String types) {
List<BSONObject> products = commonDataService.getData(ids, types);
return products;
}
Try the following configuration, it sets up the default Content negotiation strategy(based on article here):
#Configuration
#EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.APPLICATION_JSON);
}
}
Another option will be to provide other ways of specifying the content format, if Accept header is not feasible, an option could be to specify an extension /myuri/sample.json which would be returned as a json.