The project includes a collection of micro services built on Spring Cloud that support photo uploading, storage to Azure blob, analysis with cognitive service and storage of analysis results to Azure SQL server. Image storage service and image analysis service use Spring Cloud Stream Azure Event Hub binder to bind Azure Event Hub as message channel.
- Java 8
- Maven
- Postman https://www.getpostman.com/apps
-
notificationReceiver
ofimage-analyzer
has aStreamListener
(annotated method e.g. onReceive) listening to ainput
channel also backed by Azure Event Hub. Theinput
channel is configured inspring.cloud.stream.bindings.input.destination
- the sameString
value ofoutput
channel as they are working against the same channel. -
Full blob URL then is sent as part of HttpPost request to Cognitive service REST API via a
@Cacheable("azureCache")
annotated methodString analyze(String imageURL)
and analysis result e.g.[{"confidence":0.870581954633533,"text":"a bowl of oranges on a table"}]
is then cached into Azure Redis Cache service with cache keyazureCache::imageURL
.
-
image-analyzer
(modules:azure-eventhub-binder
,azure-storage
, port:8080
)image-storage
(modules:azure-eventhub-binder
,azure-storage
,azure-sql-server
, port:8090
).
-
$ az account set --subscription <name or id> $ az ad sp create-for-rbac --sdk-auth > my.azureauth
-
server.port=8080 spring.cloud.azure.credentialFilePath=my.azureauth spring.cloud.azure.resourceGroup=springone-cloud spring.cloud.azure.region=eastus spring.cloud.azure.eventhub.namespace=springone-cloud-eh #Storage account name must be between 3 and 24 characters in length and use numbers and lower-case letters only. spring.cloud.stream.eventhub.checkpointStorageAccount=springonecloudstorage # For example here, the destination name of input and output should be the same. # Eventhub name can contain only lowercase letters, numbers, and the dash (-) character. # Every dash (-) character must be immediately preceded and followed by a letter or number. # Must be from 3 to 63 characters long. # for storage application, it only uses output, input.destination is actually not used/needed # Not needed # spring.cloud.stream.bindings.input.destination=springone-cloud-eh-channel # spring.cloud.stream.bindings.input.group=springone-cloud-eh-consumergroup spring.cloud.stream.bindings.output.destination=springone-cloud-eh-channel spring.cloud.azure.storage.account=springonecloudstorage
public interface ResourceStorage { Resource load(String location); void store(String location, InputStream inputStream) throws IOException; }
@Component public class BlobResourceStorage implements ResourceStorage { @Autowired ResourceLoader resourceLoader; @Override public Resource load(String blobLocation) { return this.resourceLoader.getResource(blobLocation); } @Override public void store(String blobLocation, InputStream blobInputStream) throws IOException { Resource blob = load(blobLocation); OutputStream os = ((WritableResource)blob).getOutputStream(); IOUtils.copy(blobInputStream, os); os.close(); blobInputStream.close(); } }
@EnableBinding(Source.class) @Component public class NotificationSender { private static final Logger LOGGER = LoggerFactory.getLogger(NotificationSender.class); @Autowired ResourceLoader resourceLoader; @Autowired Source sender; @Autowired MessageChannel output; public void doSend(String blobLocation) { //this.sender.output().send(new GenericMessage<>(blobLocation)); this.output.send(new GenericMessage<>(blobLocation)); LOGGER.info("Payload: '" + blobLocation + "' sent!"); } }
@RestController public class Controller { @RequestMapping(value = "/", method = RequestMethod.GET) public String sayHello() { return "Say Hello World!"; } @Autowired public ResourceStorage resourceStorageService; @Autowired public NotificationSender notificationSender; @GetMapping("/images/{imageName}") @ResponseBody public ResponseEntity<Resource> serveImage(@PathVariable String imageName) throws IOException { Resource image = resourceStorageService.load("blob://images/" + imageName); String contentType = "application/octet-stream"; return ResponseEntity.ok() .contentType(MediaType.parseMediaType(contentType)) .body(new InputStreamResource(image.getInputStream())); } @PostMapping("/upload") public String updateImage(@RequestParam("image") MultipartFile image) throws IOException { String imageLocation = "blob://images/" + image.getOriginalFilename(); //1. store image resourceStorageService.store(imageLocation, image.getInputStream()); //2. send notification notificationSender.doSend(imageLocation); return "redirect:/"; } }
-
server.port=8090 spring.cloud.azure.credentialFilePath=my.azureauth spring.cloud.azure.resourceGroup=springone-cloud spring.cloud.azure.region=eastus spring.cloud.azure.eventhub.namespace=springone-cloud-eh #Storage account name must be between 3 and 24 characters in length and use numbers and lower-case letters only. spring.cloud.stream.eventhub.checkpointStorageAccount=springonecloudstorage # For example here, the destination name of input and output should be the same. # Eventhub name can contain only lowercase letters, numbers, and the dash (-) character. # Every dash (-) character must be immediately preceded and followed by a letter or number. # Must be from 3 to 63 characters long. spring.cloud.stream.bindings.input.destination=springone-cloud-eh-channel spring.cloud.stream.bindings.input.group=springone-cloud-eh-consumergroup # Not needed # spring.cloud.stream.bindings.output.destination=springone-cloud-eh-channel spring.cloud.azure.storage.account=springonecloudstorage #Azure Redis cache spring.cloud.azure.redis.name=springone-cloud-redis
@EnableBinding(Sink.class) public class NotificationReceiver { private static final Logger LOGGER = LoggerFactory.getLogger(NotificationReceiver.class); @Autowired ResourceLoader resourceLoader; @Autowired ImageAnalysisService svc; @StreamListener(Sink.INPUT) public void onReceive(String blobLocaiton) throws IOException, URISyntaxException, JSONException { LOGGER.info("Received: " + blobLocaiton); Resource blobResource = this.resourceLoader.getResource(blobLocaiton); String res = this.svc.analyze(blobResource.getURL().toString()); LOGGER.info(res); } }
@Component public class ImageAnalysisService { private static final Logger LOGGER = LoggerFactory.getLogger(ImageAnalysisService.class); private String apiKey = "6cbdab0344de4c6aba54caa60fc57567"; private String uriBase = "https://eastus.api.cognitive.microsoft.com/vision/v1.0/analyze"; @Cacheable("azureCache") public String analyze(String imageURL) throws URISyntaxException, IOException, JSONException { CloseableHttpClient httpclient = HttpClients.createDefault(); URIBuilder builder = new URIBuilder(uriBase); // Request parameters. All of them are optional. builder.setParameter("visualFeatures", "Categories,Description,Color"); builder.setParameter("language", "en"); // Prepare the URI for the REST API call. URI uri = builder.build(); HttpPost request = new HttpPost(uri); // Request headers. request.setHeader("Content-Type", "application/json"); request.setHeader("Ocp-Apim-Subscription-Key", apiKey); // Request body. StringEntity reqEntity = new StringEntity("{\"url\":\"" + imageURL + "\"}"); request.setEntity(reqEntity); // Execute the REST API call and get the response entity. HttpResponse response = httpclient.execute(request); if(response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { HttpEntity entity = response.getEntity(); if (entity != null) { JSONObject json = new JSONObject(EntityUtils.toString(entity)); return json.getJSONObject("description").get("captions").toString(); } } LOGGER.info("Analysis result is null"); return ""; } }
-
c.e.springonedemo.NotificationReceiver : Received: blob://images/orange.jpg c.e.springonedemo.ImageAnalysisService : [{"confidence":0.870581954633533,"text":"a bowl of oranges on a table"}]
... 3) "azureCache::http://springonecloudstorage.blob.core.windows.net/images/orange.jpg" ...