Participate in DZone Research Surveys: You Can Shape Trend Reports! (+ Enter the Raffles)
Introduction To Artificial Intelligence With Code: Part 1
Modern API Management
When assessing prominent topics across DZone — and the software engineering space more broadly — it simply felt incomplete to conduct research on the larger impacts of data and the cloud without talking about such a crucial component of modern software architectures: APIs. Communication is key in an era when applications and data capabilities are growing increasingly complex. Therefore, we set our sights on investigating the emerging ways in which data that would otherwise be isolated can better integrate with and work alongside other app components and across systems.For DZone's 2024 Modern API Management Trend Report, we focused our research specifically on APIs' growing influence across domains, prevalent paradigms and implementation techniques, security strategies, AI, and automation. Alongside observations from our original research, practicing tech professionals from the DZone Community contributed articles addressing key topics in the API space, including automated API generation via no and low code; communication architecture design among systems, APIs, and microservices; GraphQL vs. REST; and the role of APIs in the modern cloud-native landscape.
Open Source Migration Practices and Patterns
MongoDB Essentials
Microsoft's SQL Server is a powerful RDBMS that is extensively utilized in diverse industries for the purposes of data storage, retrieval, and analysis. The objective of this article is to assist novices in comprehending SQL Server from fundamental principles to advanced techniques, employing real-world illustrations derived from nProbe data. nProbe is a well-known network traffic monitoring tool that offers comprehensive insights into network traffic patterns. Getting Started With SQL Server 1. Introduction to SQL Server SQL Server provides a comprehensive database management platform that integrates advanced analytics, robust security features, and extensive reporting capabilities. It offers support for a wide range of data types and functions, enabling efficient data management and analysis. 2. Installation Begin by installing SQL Server. Microsoft offers different editions, including Express, Standard, and Enterprise, to cater to varying needs. The Express edition is free and suitable for learning and small applications. Here is the step-by-step guide to install the SQL server. 3. Basic SQL Operations Learn the fundamentals of SQL, including creating databases, tables, and writing basic queries: Create database: `CREATE DATABASE TrafficData;` Create table: Define a table structure to store nProbe data: MS SQL CREATE TABLE NetworkTraffic ( ID INT PRIMARY KEY, SourceIP VARCHAR(15), DestinationIP VARCHAR(15), Packets INT, Bytes BIGINT, Timestamp DATETIME ); Intermediate SQL Techniques 4. Data Manipulation Inserting Data To insert data into the `NetworkTraffic` table, you might collect information from various sources, such as network sensors or logs. MS SQL INSERT INTO NetworkTraffic (SourceIP, DestinationIP, Packets, Bytes, Timestamp) VALUES ('10.0.0.1', '192.168.1.1', 150, 2048, '2023-10-01T14:30:00'); Batch insert to minimize the impact on database performance: MS SQL INSERT INTO NetworkTraffic (SourceIP, DestinationIP, Packets, Bytes, Timestamp) VALUES ('10.0.0.2', '192.168.1.2', 50, 1024, '2023-10-01T15:00:00'), ('10.0.0.3', '192.168.1.3', 100, 1536, '2023-10-01T15:05:00'), ('10.0.0.4', '192.168.1.4', 200, 4096, '2023-10-01T15:10:00'); Updating Data You may need to update records as new data becomes available or corrections are necessary. For instance, updating the byte count for a particular traffic record: MS SQL UPDATE NetworkTraffic SET Bytes = 3072 WHERE ID = 1; Update multiple fields at once: MS SQL UPDATE NetworkTraffic SET Packets = 180, Bytes = 3072 WHERE SourceIP = '10.0.0.1' AND Timestamp = '2023-10-01T14:30:00'; Deleting Data Removing data is straightforward but should be handled with caution to avoid accidental data loss. MS SQL DELETE FROM NetworkTraffic WHERE Timestamp < '2023-01-01'; Conditional delete based on network traffic analysis: MS SQL DELETE FROM NetworkTraffic WHERE Bytes < 500 AND Timestamp BETWEEN '2023-01-01' AND '2023-06-01'; Querying Data Simple Queries: Retrieve basic information from your data set. MS SQL SELECT FROM NetworkTraffic WHERE SourceIP = '10.0.0.1'; Select specific columns: MS SQL SELECT SourceIP, DestinationIP, Bytes FROM NetworkTraffic WHERE Bytes > 2000; Aggregate Functions Useful for summarizing or analyzing large data sets. MS SQL SELECT AVG(Bytes), MAX(Bytes), MIN(Bytes) FROM NetworkTraffic WHERE Timestamp > '2023-01-01'; Grouping data for more detailed analysis: MS SQL SELECT SourceIP, AVG(Bytes) AS AvgBytes FROM NetworkTraffic GROUP BY SourceIP HAVING AVG(Bytes) > 1500; Join Operations In scenarios where you have multiple tables, joins are essential. Assume another table `IPDetails` that stores additional information about each IP. MS SQL SELECT n.SourceIP, n.DestinationIP, n.Bytes, i.Location FROM NetworkTraffic n JOIN IPDetails i ON n.SourceIP = i.IPAddress WHERE n.Bytes > 1000; Complex Queries Combining multiple SQL operations to extract in-depth insights. MS SQL SELECT SourceIP, SUM(Bytes) AS TotalBytes FROM NetworkTraffic WHERE Timestamp BETWEEN '2023-01-01' AND '2023-02-01' GROUP BY SourceIP ORDER BY TotalBytes DESC; Advanced SQL Server Features 5. Indexing for Performance Optimizing SQL Server performance through indexing and leveraging stored procedures for automation is critical for managing large databases efficiently. Here’s an in-depth look at both topics, with practical examples, particularly focusing on enhancing operations within a network traffic database like the one collected from nProbe. Why Indexing Matters Indexing is a strategy to speed up the retrieval of records from a database by reducing the number of disk accesses required when a query is processed. It is especially vital in databases with large volumes of data, where search operations can become increasingly slow. Types of Indexes Clustered indexes: Change the way records are stored in the database as they sort and store the data rows in the table based on their key values. Tables can have only one clustered index. Non-clustered indexes: Do not alter the physical order of the data, but create a logical ordering of the data rows and use pointers to physical rows; each table can have multiple non-clustered indexes. Example: Creating an Index on Network Traffic Data Suppose you frequently query the `NetworkTraffic` table to fetch records based on `SourceIP` and `Timestamp`. You can create a non-clustered index to speed up these queries: MS SQL CREATE NONCLUSTERED INDEX idx_networktraffic_sourceip ON NetworkTraffic (SourceIP, Timestamp); This index would particularly improve performance for queries that look up records by `SourceIP` and filter on `Timestamp`, as the index helps locate data quickly without scanning the entire table. Below are additional instructions on utilizing indexing effectively. 6. Stored Procedures and Automation Benefits of Using Stored Procedures Stored procedures help in encapsulating SQL code for reuse and automating routine operations. They enhance security, reduce network traffic, and improve performance by minimizing the amount of information sent to the server. Example: Creating a Stored Procedure Imagine you often need to insert new records into the `NetworkTraffic` table. A stored procedure that encapsulates the insert operation can simplify the addition of new records: MS SQL CREATE PROCEDURE AddNetworkTraffic @SourceIP VARCHAR(15), @DestinationIP VARCHAR(15), @Packets INT, @Bytes BIGINT, @Timestamp DATETIME AS BEGIN INSERT INTO NetworkTraffic (SourceIP, DestinationIP, Packets, Bytes, Timestamp) VALUES (@SourceIP, @DestinationIP, @Packets, @Bytes, @Timestamp); END; Using the Stored Procedure To insert a new record, instead of writing a full insert query, you simply execute the stored procedure: MS SQL EXEC AddNetworkTraffic @SourceIP = '192.168.1.1', @DestinationIP = '10.0.0.1', @Packets = 100, @Bytes = 2048, @Timestamp = '2024-04-12T14:30:00'; Automation Example: Scheduled Tasks SQL Server Agent can be used to schedule the execution of stored procedures. For instance, you might want to run a procedure that cleans up old records every night: MS SQL CREATE PROCEDURE CleanupOldRecords AS BEGIN DELETE FROM NetworkTraffic WHERE Timestamp < DATEADD(month, -1, GETDATE()); END; You can schedule this procedure to run automatically at midnight every day using SQL Server Agent, ensuring that your database does not retain outdated records beyond a certain period. By implementing proper indexing strategies and utilizing stored procedures, you can significantly enhance the performance and maintainability of your SQL Server databases. These practices are particularly beneficial in environments where data volumes are large and efficiency is paramount, such as in managing network traffic data for IFC systems. 7. Performance Tuning and Optimization Performance tuning and optimization in SQL Server are critical aspects that involve a systematic review of database and system settings to improve the efficiency of your operations. Proper tuning not only enhances the speed and responsiveness of your database but also helps in managing resources more effectively, leading to cost savings and improved user satisfaction. Key Areas for Performance Tuning and Optimization 1. Query Optimization Optimize queries: The first step in performance tuning is to ensure that the queries are as efficient as possible. This includes selecting the appropriate columns, avoiding unnecessary calculations, and using joins effectively. Query profiling: SQL Server provides tools like SQL Server Profiler and Query Store that help identify slow-running queries and bottlenecks in your SQL statements. Example: Here’s how you can use the Query Store to find performance issues: MS SQL SELECT TOP 10 qt.query_sql_text, rs.avg_duration FROM sys.query_store_query_text AS qt JOIN sys.query_store_plan AS qp ON qt.query_text_id = qp.query_text_id JOIN sys.query_store_runtime_stats AS rs ON qp.plan_id = rs.plan_id ORDER BY rs.avg_duration DESC; 2. Index Management Review and adjust indexes: Regularly reviewing the usage and effectiveness of indexes is vital. Unused indexes should be dropped, and missing indexes should be added where significant performance gains can be made. Index maintenance: Rebuilding and reorganizing indexes can help in maintaining performance, especially in databases with heavy write operations. Example: Rebuild an index using T-SQL: MS SQL ALTER INDEX ALL ON dbo.YourTable REBUILD WITH (FILLFACTOR = 90, SORT_IN_TEMPDB = ON, STATISTICS_NORECOMPUTE = OFF); 3. Database Configuration and Maintenance Database settings: Adjust database settings such as recovery model, file configuration, and buffer management to optimize performance. Routine maintenance: Implement regular maintenance plans that include updating statistics, checking database integrity, and cleaning up old data. Example: Set up a maintenance plan in SQL Server Management Studio (SSMS) using the Maintenance Plan Wizard. 4. Hardware and Resource Optimization Hardware upgrades: Sometimes, the best way to achieve performance gains is through hardware upgrades, such as increasing memory, adding faster disks, or upgrading CPUs. Resource allocation: Ensure that the SQL Server has enough memory and CPU resources allocated, particularly in environments where the server hosts multiple applications. Example: Configure maximum server memory: MS SQL EXEC sp_configure 'max server memory', 4096; RECONFIGURE; 5. Monitoring and Alerts System monitoring: Continuous monitoring of system performance metrics is crucial. Tools like System Monitor (PerfMon) and Dynamic Management Views (DMVs) in SQL Server provide real-time data about system health. Alerts setup: Configure alerts for critical conditions, such as low disk space, high CPU usage, or blocking issues, to ensure that timely actions are taken. Example: Set up an alert in SQL Server Agent: MS SQL USE msdb ; GO EXEC dbo.sp_add_alert @name = N'High CPU Alert', @message_id = 0, @severity = 0, @enabled = 1, @delay_between_responses = 0, @include_event_description_in = 1, @notification_message = N'SQL Server CPU usage is high.', @performance_condition = N'SQLServer:SQL Statistics|Batch Requests/sec|_Total|>|1000', @job_id = N'00000000-1111-2222-3333-444444444444'; GO Performance tuning and optimization is an ongoing process, requiring regular adjustments and monitoring. By systematically addressing these key areas, you can ensure that your SQL Server environment is running efficiently, effectively supporting your organizational needs. Conclusion Mastering SQL Server is a journey that evolves with practice and experience. Starting from basic operations to leveraging advanced features, SQL Server provides a powerful toolset for managing and analyzing data. As your skills progress, you can handle larger datasets like those from nProbe, extracting valuable insights and improving your network's performance and security. For those looking to dive deeper, Microsoft offers extensive documentation and a community rich with resources to explore more complex SQL Server capabilities. Useful References nProbe SQL Server SQL server performance tuning
Failures in software systems are inevitable. How these failures are handled can significantly impact system performance, reliability, and the business’s bottom line. In this post, I want to discuss the upside of failure. Why you should seek failure, why failure is good, and why avoiding failure can reduce the reliability of your application. We will start with the discussion of fail-fast vs. fail-safe, this will take us to the second discussion about failures in general. As a side note, if you like the content of this and the other posts in this series check out my Debugging book that covers this subject. If you have friends that are learning to code I'd appreciate a reference to my Java Basics book. If you want to get back to Java after a while check out my Java 8 to 21 book. Fail-Fast Fail-fast systems are designed to immediately stop functioning upon encountering an unexpected condition. This immediate failure helps to catch errors early, making debugging more straightforward. The fail-fast approach ensures that errors are caught immediately. For example, in the world of programming languages, Java embodies this approach by producing a NullPointerException instantly when encountering a null value, stopping the system, and making the error clear. This immediate response helps developers identify and address issues quickly, preventing them from becoming more serious. By catching and stopping errors early, fail-fast systems reduce the risk of cascading failures, where one error leads to others. This makes it easier to contain and resolve issues before they spread through the system, preserving overall stability. It is easy to write unit and integration tests for fail-fast systems. This advantage is even more pronounced when we need to understand the test failure. Fail-fast systems usually point directly at the problem in the error stack trace. However, fail-fast systems carry their own risks, particularly in production environments: Production disruptions: If a bug reaches production, it can cause immediate and significant disruptions, potentially impacting both system performance and the business’s operations. Risk appetite: Fail-fast systems require a level of risk tolerance from both engineers and executives. They need to be prepared to handle and address failures quickly, often balancing this with potential business impacts. Fail-Safe Fail-safe systems take a different approach, aiming to recover and continue even in the face of unexpected conditions. This makes them particularly suited for uncertain or volatile environments. Microservices are a prime example of fail-safe systems, embracing resiliency through their architecture. Circuit breakers, both physical and software-based, disconnect failing functionality to prevent cascading failures, helping the system continue operating. Fail-safe systems ensure that systems can survive even harsh production environments, reducing the risk of catastrophic failure. This makes them particularly suited for mission-critical applications, such as in hardware devices or aerospace systems, where smooth recovery from errors is crucial. However, fail-safe systems have downsides: Hidden errors: By attempting to recover from errors, fail-safe systems can delay the detection of issues, making them harder to trace and potentially leading to more severe cascading failures. Debugging challenges: This delayed nature of errors can complicate debugging, requiring more time and effort to find and resolve issues. Choosing Between Fail-Fast and Fail-Safe It's challenging to determine which approach is better, as both have their merits. Fail-fast systems offer immediate debugging, lower risk of cascading failures, and quicker detection and resolution of bugs. This helps catch and fix issues early, preventing them from spreading. Fail-safe systems handle errors gracefully, making them better suited for mission-critical systems and volatile environments, where catastrophic failures can be devastating. Balancing Both To leverage the strengths of each approach, a balanced strategy can be effective: Fail-fast for local services: When invoking local services like databases, fail-fast can catch errors early, preventing cascading failures. Fail-safe for remote resources: When relying on remote resources, such as external web services, fail-safe can prevent disruptions from external failures. A balanced approach also requires clear and consistent implementation throughout coding, reviews, tooling, and testing processes, ensuring it is integrated seamlessly. Fail-fast can integrate well with orchestration and observability. Effectively, this moves the fail-safe aspect to a different layer of OPS instead of into the developer layer. Consistent Layer Behavior This is where things get interesting. It isn't about choosing between fail-safe and fail-fast. It's about choosing the right layer for them. E.g. if an error is handled in a deep layer using a fail-safe approach, it won't be noticed. This might be OK, but if that error has an adverse impact (performance, garbage data, corruption, security, etc.) then we will have a problem later on and won't have a clue. The right solution is to handle all errors in a single layer, in modern systems the top layer is the OPS layer and it makes the most sense. It can report the error to the engineers who are most qualified to deal with the error. But they can also provide immediate mitigation such as restarting a service, allocating additional resources, or reverting a version. Retry’s Are Not Fail-Safe Recently I was at a lecture where the speakers listed their updated cloud architecture. They chose to take a shortcut to microservices by using a framework that allows them to retry in the case of failure. Unfortunately, failure doesn't behave the way we would like. You can't eliminate it completely through testing alone. Retry isn't fail-safe. In fact: it can mean catastrophe. They tested their system and "it works", even in production. But let's assume that a catastrophic situation does occur, their retry mechanism can operate as a denial of service attack against their own servers. The number of ways in which ad-hoc architectures such as this can fail is mind-boggling. This is especially important once we redefine failures. Redefining Failure Failures in software systems aren't just about crashes. A crash can be seen as a simple and immediate failure, but there are more complex issues to consider. In fact, crashes in the age of containers are probably the best failures. A system restarts seamlessly with barely an interruption. Data Corruption Data corruption is far more severe and insidious than a crash. It carries with it long-term consequences. Corrupted data can lead to security and reliability problems that are challenging to fix, requiring extensive reworking and potentially unrecoverable data. Cloud computing has led to defensive programming techniques, like circuit breakers and retries, emphasizing comprehensive testing and logging to catch and handle failures gracefully. In a way, this environment sent us back in terms of quality. A fail-fast system at the data level could stop this from happening. Addressing a bug goes beyond a simple fix. It requires understanding its root cause and preventing reoccurrence, extending into comprehensive logging, testing, and process improvements. This ensures that the bug is fully addressed, reducing the chances of it reoccurring. Don't Fix the Bug If it's a bug in production you should probably revert, if you can't instantly revert production. This should always be possible and if it isn't this is something you should work on. Failures must be fully understood before a fix is undertaken. In my own companies, I often skipped that step due to pressure, in a small startup that is forgivable. In larger companies, we need to understand the root cause. A culture of debriefing for bugs and production issues is essential. The fix should also include process mitigation that prevents similar issues from reaching production. Debugging Failure Fail-fast systems are much easier to debug. They have inherently simpler architecture and it is easier to pinpoint an issue to a specific area. It is crucial to throw exceptions even for minor violations (e.g. validations). This prevents cascading types of bugs that prevail in loose systems. This should be further enforced by unit tests that verify the limits we define and verify proper exceptions are thrown. Retries should be avoided in the code as they make debugging exceptionally difficult and their proper place is in the OPS layer. To facilitate that further, timeouts should be short by default. Avoiding Cascading Failure Failure isn't something we can avoid, predict, or fully test against. The only thing we can do is soften the blow when a failure occurs. Often this "softening" is achieved by using long-running tests meant to replicate extreme conditions as much as possible with the goal of finding our application's weak spots. This is rarely enough, robust systems need to revise these tests often based on real production failures. A great example of a fail-safe would be a cache of REST responses that lets us keep working even when a service is down. Unfortunately, this can lead to complex niche issues such as cache poisoning or a situation in which a banned user still had access due to cache. Hybrid in Production Fail-safe is best applied only in production/staging and in the OPS layer. This reduces the amount of changes between production and dev, we want them to be as similar as possible, yet it's still a change that can negatively impact production. However, the benefits are tremendous as observability can get a clear picture of system failures. The discussion here is a bit colored by my more recent experience of building observable cloud architectures. However, the same principle applies to any type of software whether embedded or in the cloud. In such cases we often choose to implement fail-safe in the code, in this case, I would suggest implementing it consistently and consciously in a specific layer. There's also a special case of libraries/frameworks that often provide inconsistent and badly documented behaviors in these situations. I myself am guilty of such inconsistency in some of my work. It's an easy mistake to make. Final Word This is my last post on the theory of debugging series that's part of my book/course on debugging. We often think of debugging as the action we take when something fails, it isn't. Debugging starts the moment we write the first line of code. We make decisions that will impact the debugging process as we code, often we're just unaware of these decisions until we get a failure. I hope this post and series will help you write code that is prepared for the unknown. Debugging, by its nature, deals with the unexpected. Tests can't help. But as I illustrated in my previous posts, there are many simple practices we can undertake that would make it easier to prepare. This isn't a one-time process, it's an iterative process that requires re-evaluation of decisions made as we encounter failure.
Do you want to learn how to create Tweets from a Java application using the X (Twitter) API v2? This blog will show you in a step-by-step guide how to do so. Enjoy! Introduction X (Twitter) provides an API that allows you to interact with your account from an application. Currently, two versions exist. In this blog, you will use the most recent X API v2. Although a lot of information can be found on how to set up your environment and how to interact with the API, it took me quite some time to do so from within a Java application. In this blog, you will learn how to set up your account and how you can create tweets from a Java application. The sources for this blog can be found on GitHub. Prerequisites Prerequisites for this blog are: Basic knowledge of Java, Java 21 is used; An X account; A website you own (not mandatory, but for security reasons the better). Set up Developer Account The first thing to do is to set up a developer account. Navigate to the sign-up page. Beware that there exist multiple types of accounts: free account, basic account, pro account, and enterprise account. Scroll down all the way to the bottom of the page and choose to create a free account by clicking the button Sign up Free Account. You will need to describe your use case using at least 250 characters. After signing up, you end up in the developer portal. Create Project and App With the Free tier, you can create one Project and one App. Create the Project and the App. Authentication As you are going to create Tweets for a user, you will need to set up the authentication using OAuth 2.0 Authorization Code Flow with PKCE. However, it is important that you have configured your App correctly. Navigate to your App in the developer portal and click the Edit button in the User authentication settings. Different sections are available here where you are required to add information and to choose options. App Permissions These permissions enable OAuth 1.0a Authentication. It is confusing that you need to check one of these bullets because OAuth 1.0a Authentication will not be used in your use case. However, because you will create a tweet, select Read and Write, just to be sure. Type of App The type of App will enable OAuth 2.0 Authentication, this is the one you will use. You will invoke the API from an application. Therefore, choose a Web App, Automated App, or Bot. App Info In the App info section, you need to provide a Callback URI and a Website URL. The Callback URI is important as you will see later on in the next paragraphs. Fill in the URL of your website. You can use any URL, but the Callback URI will be used to provide you with an access token, so it is better to use the URL of a website you own. Click the Save button to save your changes. A Client ID and Client Secret are generated and save them. Create Tweet Everything is set up in the developer portal. Now it is time to create the Java application in order to be able to create a Tweet. Twitter API Client Library for Java In order to create the tweet, you will make use of the Twitter API Client Library for Java. Beware that, at the time of writing, the library is still in beta. The library also only supports the X API v2 endpoints, but that is exactly the endpoints you will be using, so that is not a problem. Add the dependency to the pom file: XML <dependency> <groupId>com.twitter</groupId> <artifactId>twitter-api-java-sdk</artifactId> <version>2.0.3</version> </dependency> Authorization In order to be able to create tweets on behalf of your account, you need to authorize the App. The source code below is based on the example provided in the SDK. You need the Client ID and Client Secret you saved before. If you lost them, you can generate a new secret in the developer portal. Navigate to your App, and click the Keys and Tokens tab. Scroll down retrieve the Client ID and generate a new Client Secret. The main method executes the following steps: Set the correct credentials as environment variables: TWITTER_OAUTH2_CLIENT_ID: the OAuth 2.0 client ID TWITTER_OAUTH2_CLIENT_SECRET: the Oauth 2.0 Client Secret TWITTER_OAUTH2_ACCESS_TOKEN: leave it blank TWITTER_OAUTH2_REFRESH_TOKEN: leave it blank Authorize the App and retrieve an access and refresh token. Set the newly received access and refresh token in the credentials object. Call the X API in order to create the tweet. Java public static void main(String[] args) { TwitterCredentialsOAuth2 credentials = new TwitterCredentialsOAuth2(System.getenv("TWITTER_OAUTH2_CLIENT_ID"), System.getenv("TWITTER_OAUTH2_CLIENT_SECRET"), System.getenv("TWITTER_OAUTH2_ACCESS_TOKEN"), System.getenv("TWITTER_OAUTH2_REFRESH_TOKEN")); OAuth2AccessToken accessToken = getAccessToken(credentials); if (accessToken == null) { return; } // Setting the access & refresh tokens into TwitterCredentialsOAuth2 credentials.setTwitterOauth2AccessToken(accessToken.getAccessToken()); credentials.setTwitterOauth2RefreshToken(accessToken.getRefreshToken()); callApi(credentials); } The getAccessToken method executes the following steps: Creates a Twitter service object: Set the Callback URI to the one you specified in the developer portal. Set the scopes (what is allowed) you want to authorize. By using offline.access, you will receive a refresh token which allows you to retrieve a new access token without prompting the user via the refresh token flow. This means that you can continuously create tweets without the need of user interaction. An authorization URL is provided to you where you will authorize the App for the requested scopes. You are redirected to the Callback URI and in the URL the authorization code will be visible. The getAccessToken method waits until you copy the authorization code and hit enter. The access token and refresh token are printed to the console and returned from the method. Java private static OAuth2AccessToken getAccessToken(TwitterCredentialsOAuth2 credentials) { TwitterOAuth20Service service = new TwitterOAuth20Service( credentials.getTwitterOauth2ClientId(), credentials.getTwitterOAuth2ClientSecret(), "<Fill in your Callback URI as configured in your X App in the developer portal>", "offline.access tweet.read users.read tweet.write"); OAuth2AccessToken accessToken = null; try { final Scanner in = new Scanner(System.in, "UTF-8"); System.out.println("Fetching the Authorization URL..."); final String secretState = "state"; PKCE pkce = new PKCE(); pkce.setCodeChallenge("challenge"); pkce.setCodeChallengeMethod(PKCECodeChallengeMethod.PLAIN); pkce.setCodeVerifier("challenge"); String authorizationUrl = service.getAuthorizationUrl(pkce, secretState); System.out.println("Go to the Authorization URL and authorize your App:\n" + authorizationUrl + "\nAfter that paste the authorization code here\n>>"); final String code = in.nextLine(); System.out.println("\nTrading the Authorization Code for an Access Token..."); accessToken = service.getAccessToken(pkce, code); System.out.println("Access token: " + accessToken.getAccessToken()); System.out.println("Refresh token: " + accessToken.getRefreshToken()); } catch (Exception e) { System.err.println("Error while getting the access token:\n " + e); e.printStackTrace(); } return accessToken; } Now that you authorized the App, you are able to see that you have done so in your X settings. Navigate to Settings and Privacy in your X account. Navigate to Security and account access. Navigate to Apps and sessions. Navigate to Connected apps. Here you will find the App you authorized and which authorizations it has. The callApi method executes the following steps: Create a TwitterApi instance with the provided credentials. Create a TweetRequest. Create the Tweet. Java private static void callApi(TwitterCredentialsOAuth2 credentials) { TwitterApi apiInstance = new TwitterApi(credentials); TweetCreateRequest tweetCreateRequest = new TweetCreateRequest(); // TweetCreateRequest | tweetCreateRequest.setText("Hello World!"); try { TweetCreateResponse result = apiInstance.tweets().createTweet(tweetCreateRequest) .execute(); System.out.println(result); } catch (ApiException e) { System.err.println("Exception when calling TweetsApi#createTweet"); System.err.println("Status code: " + e.getCode()); System.err.println("Reason: " + e.getResponseBody()); System.err.println("Response headers: " + e.getResponseHeaders()); e.printStackTrace(); } } Add an sdk.properties file to the root of the repository, otherwise, an Exception will be thrown (the Exception is not blocking, but spoils the output). If everything went well, you now have created your first Tweet! Obtain New Access Token You only need to execute the source code above once. The retrieved access token, however, will only stay valid for two hours. After that time (or earlier), you need to retrieve a new access token using the refresh token. The source code below is based on the example provided in the SDK. The main method executes the following steps: Set the credentials including the access and refresh token you obtained in the previous sections. Add a callback to the TwitterApi instance in order to retrieve a new access and refresh token. Request to refresh the token, the callback method MainToken will set the new tokens and again a Tweet can be created. Java public static void main(String[] args) { TwitterApi apiInstance = new TwitterApi(new TwitterCredentialsOAuth2(System.getenv("TWITTER_OAUTH2_CLIENT_ID"), System.getenv("TWITTER_OAUTH2_CLIENT_SECRET"), System.getenv("TWITTER_OAUTH2_ACCESS_TOKEN"), System.getenv("TWITTER_OAUTH2_REFRESH_TOKEN"))); apiInstance.addCallback(new MaintainToken()); try { apiInstance.refreshToken(); } catch (Exception e) { System.err.println("Error while trying to refresh existing token : " + e); e.printStackTrace(); return; } callApi(apiInstance); } The MaintainToken method only sets the new tokens. Java class MaintainToken implements ApiClientCallback { @Override public void onAfterRefreshToken(OAuth2AccessToken accessToken) { System.out.println("access: " + accessToken.getAccessToken()); System.out.println("refresh: " + accessToken.getRefreshToken()); } } Conclusion In this blog, you learned how to configure an App in the developer portal. You learned how to authorize your App from a Java application and how to create a Tweet.
In the previous post — Understanding Prompt Injection and Other Risks of Generative AI, you learned about the security risks, vulnerabilities, and mitigations associated with Generative AI. The article elaborated on prompt injection and introduced other security risks. In this article, you will learn about specialized tools and frameworks for prompt inspection and protection or AI firewalls. The Rise of Generative AI and Emerging Security Challenges The rapid advancements in generative artificial intelligence (AI) have ushered in an era of unprecedented creativity and innovation. Along with the advancements, this transformative technology has also introduced a host of new security challenges that demand urgent attention. As AI systems become increasingly sophisticated, capable of autonomously generating content ranging from text to images and videos, the potential for malicious exploitation has grown exponentially. Threat actors, including cybercriminals and nation-state actors, have recognized the power of these generative AI tools and are actively seeking to leverage them for nefarious purposes. In particular, Generative AI, powered by deep learning architectures such as Generative Adversarial Networks (GANs) and language models like GPT (Generative Pre-trained Transformer), has unlocked remarkable capabilities in content creation, text generation, image synthesis, and more. While these advancements hold immense promise for innovation and productivity, they also introduce profound security challenges. AI-Powered Social Engineering Attacks One of the primary concerns is the rise of AI-powered social engineering attacks. Generative AI can be used to create highly convincing and personalized phishing emails, deepfakes, and other forms of manipulated content that can deceive even the most vigilant individuals. These attacks can be deployed at scale, making them a formidable threat to both individuals and organizations. Vulnerabilities in AI-Integrated Applications The integration of large language models (LLMs) into critical applications, such as chatbots and virtual assistants, introduces new vulnerabilities. Adversaries can exploit these models through techniques like prompt injection, where they can coerce the AI system to generate harmful or sensitive outputs. The potential for data poisoning, where malicious data is used to corrupt the training of AI models, further exacerbates the security risks. Specialized AI Security Tools and Frameworks AI Security encompasses a multifaceted approach, involving proactive measures to prevent exploitation, robust authentication mechanisms, continuous monitoring for anomalies, and rapid response capabilities. At the heart of this approach lies the concept of Prompt Inspection and Protection, akin to deploying an AI Firewall to scrutinize inputs, outputs, and processes, thereby mitigating risks and ensuring the integrity of AI systems. To address these challenges, the development of specialized tools and frameworks for Prompt Inspection and Protection, or AI Firewall, has become a crucial priority. These solutions leverage advanced AI and machine learning techniques to detect and mitigate security threats in AI applications. Robust Intelligence's AI Firewall One such tool is Robust Intelligence's AI Firewall, which provides real-time protection for AI applications by automatically configuring guardrails to address the specific vulnerabilities of each model. It covers a wide range of security and safety threats, including prompt injection attacks, insecure output handling, data poisoning, and sensitive information disclosure. Nightfall AI's Firewalls for AI Another prominent solution is Nightfall AI's Firewalls for AI, which allows organizations to safeguard their AI applications against a variety of security risks. Nightfall's platform can be deployed via API, SDK, or reverse proxy, with the API-based approach offering flexibility and ease of use for developers. Intel's AI Technologies for Network Applications Intel's AI Technologies for Network Applications also play a significant role in the AI security landscape. This suite of tools and libraries, such as the Traffic Analytics Development Kit (TADK), enables the integration of real-time AI within network security applications like web application firewalls and next-generation firewalls. These solutions leverage AI and machine learning models to detect malicious content and traffic anomalies. Broader AI Governance Frameworks and Standards Beyond these specialized tools, broader AI governance frameworks and standards, such as those from the OECD, UNESCO, and ISO/IEC, provide valuable guidance on ensuring the trustworthy and responsible development and deployment of AI systems. Companies like IBM have introduced a Framework for Securing Generative AI. These principles and guidelines can inform the overall approach to AI Firewall implementation. Additional Tools and Frameworks for AI Security Several tools and frameworks have emerged to bolster AI security and facilitate Prompt Inspection and Protection. These solutions leverage a combination of techniques, including anomaly detection, behavior analysis, and adversarial training, to fortify AI systems against threats. Among these, some notable examples include: AI Guard An integrated security platform specifically designed for AI environments, AI Guard employs advanced algorithms to detect and neutralize adversarial inputs, unauthorized access attempts, and anomalous behavior patterns in real-time. It offers seamless integration with existing AI infrastructure and customizable policies to adapt to diverse use cases. DeepShield Developed by leading AI security researchers, DeepShield is a comprehensive framework for securing deep learning models against attacks. It encompasses techniques such as input sanitization, model verification, and runtime monitoring to detect and mitigate threats proactively. DeepShield's modular architecture facilitates easy deployment across various AI applications, from natural language processing to computer vision. SentinelAI A cloud-based AI security platform, SentinelAI combines machine learning algorithms with human oversight to provide round-the-clock protection for AI systems. It offers features such as dynamic risk assessment, model explainability, and threat intelligence integration, empowering organizations to stay ahead of evolving security threats effectively. Conclusion As the generative AI era continues to unfold, the imperative for robust AI security has never been more pressing. By leveraging specialized tools and frameworks, organizations especially enterprises can safeguard their AI applications, protect sensitive data, and build resilience against the evolving threat landscape. Prompt Inspection and Protection, facilitated by the mentioned specialized tools and frameworks, serve as indispensable safeguards in this endeavor, enabling us to harness the benefits of AI innovation while safeguarding against its inherent risks. By embracing a proactive and adaptive approach to AI security, you can navigate the complexities of the Generative AI era with confidence, ensuring a safer and more resilient technological landscape for generations to come. Further Reading Firewalls for AI: The Essential Guide Framework for Securing Generative AI
What Is a Message Broker? A message broker is an important component of asynchronous distributed systems. It acts as a bridge in the producer-consumer pattern. Producers write messages to the broker, and the consumer reads the message from the broker. The broker handles queuing, routing, and delivery of messages. The diagram below shows how the broker is used in the producer-consumer pattern: This article discusses the popular brokers used today and when to use them. Simple Queue Service (SQS) Simple Queue Service (SQS) is offered by Amazon Web Services (AWS) as a managed message queue service. AWS fully manages the queue, making SQS an easy solution for passing messages between different components of software running on AWS infrastructure. The section below details what is supported and what is not in SQS Supported Pay for what you use: SQS only charges for the messages read and written to the queue. There is no recurring or base charge for using SQS. Ease of setup: SQS is a fully managed AWS service, no infrastructure setup is required for using SQS. Reading and writing are also simple either using direct REST APIs provided by SQS or using AWS lambda functions. Support for FIFO queues: Besides regular standard queues, SQS also supports FIFO queues. For applications that need strict ordering of messages, FIFO queues come in handy. Scale: SQS scales elastically with the application, no need to worry about capacity and pre-provisioning. There is no limit to the number of messages per queue, and queues offer nearly unlimited output. Queue for failed messages/dead-letter queue: All the messages that can't be processed are sent to a "dead-letter" queue. SQS takes care of moving messages automatically into the dead-letter queue based on the retry configuration of the main queue. Not Supported Lack of message broadcast: SQS doesn't have a way for multiple consumers to retrieve the same message with its "exactly once" transmission. For multiple consumer use cases, SQS needs to be used along with AWS SNS, which needs multiple queues subscribed to the same SNS topic. Replay: SQS doesn't have the ability to replay old messages. Replay is sometimes required for debugging and testing. Kinesis Kinesis is another offering of AWS. Kinesis streams enable large-scale data ingestion and real-time processing of streaming data. Like SQS, Kinesis is also a fully managed service. Below are details of what is supported and what is not in Kinesis. Supported Ease of setup: Kinesis like SQS is a fully managed AWS service, no infrastructure setup is required. Message broadcast: Kinesis allows multiple consumers to read the same message from the stream concurrently. AWS integration: Kinesis integrates seamlessly with other AWS services as part of the other AWS services. Replay: Kinesis allows the replay of messages as long as seven days in the past, and provides the ability for a client to consume messages at a later time. Real-time analytics: Provides support for ingestion, processing, and analysis of large data streams in real-time. Not Supported Strict message ordering: Kinesis supports in-order processing within a shard, however, provides no guarantee on ordering between shards. Lack of dead-letter queue: There is no support for dead dead-letter queue out of the box. Every application that consumes the stream has to deal with failure on its own. Auto-scaling: Kinesis streams don't scale dynamically in response to demand. Streams need to be provisioned ahead of time to meet the anticipated demand of both producer and consumer. Cost: For a large volume of data, pricing can be really high in comparison to other brokers. Kafka Kafka is a distributed event store and stream-processing platform. It is an open-source system developed by the Apache Software Foundation. Apache is famous for its high throughput and scalability. It excels in real-time analytics and monitoring. Below are details of what is supported and what is not in Kafka. Supported Message broadcast: Kafka allows multiple consumers to read the same message from the stream. Replay: Kafka allows messages to be replayed from a specific point in a topic. Message retention policy decides how far back a message can be replayed. Unlimited message retention: Kafka allows unlimited message retention based on the retention policy configured. Real-time analytics: Provides support for ingestion, processing, and analysis of large data streams in real-time. Open source: Kafka is an open project, which resulted in widespread adoption and community support. It has lots of configuration options available which gives the opportunity to fine-tune based on the specific use case. Not Supported Automated setup: Since Kafka is an open source, the developer needs to set up the infrastructure and Kafka cluster setup. That said, most of the public cloud providers provide managed Kafka. Simple onboarding: For Kafka clusters that are not through managed services understanding the infrastructure can become a daunting task. Apache does provide lots of documentation, but it takes time for new developers to understand. Queue semantics: In the true sense, Kafka is a distributed immutable event log, not a queuing system. It does not inherently support distributing tasks to multiple workers so that each task is processed exactly once. Dynamic partition: It is difficult to dynamically change a number of Kafka topics. This limits the scalability of the system when workload increases. The large number of partitions needs to be pre-provisioned to support the maximum load. Pulsar Pulsar is an open-source, distributed messaging and streaming platform. It is an open-source system developed by the Apache Software Foundation. It provides highly scalable, flexible, and durable for real-time data streaming and processing. Below are details of what is supported and what is not in Pulsar. Supported Multi-tenancy: Pulsar supports multi-tenancy as a first-class citizen. It provides access control across data and actions using tenant policies. Seamless geo-replication: Pulsar synchronizes data across multiple regions without any third-party replication tools. Replay: Similar to Kafka, Pulsar allows messages to be replayed from a specific point in a topic. Message retention policy decides how far back a message can be replayed. Unlimited message retention: Similar to Kafka, Pulsar allows unlimited message retention based on the retention policy configured. Flexible models: Pulsar supports both streaming and queuing models. It provides strict message ordering within a partition. Not Supported Automated setup: Similar to Kafka, Apache is open-source, and the developer needs to set up the infrastructure. Robust ecosystem: Pulsar is relatively new compared to Kafka. It doesn't have large community support similar to Kafka. Out-of-the-box Integration: Pulsar lacks out-of-the-box integration and support compared to Kafka and SQS. Conclusion Managed services require minimal maintenance effort, but non-managed services need regular, dedicated maintenance capacity. On the flip side, non-managed services provide better flexibility and tuning opportunities than managed services. In the end, choosing the right broker depends on the project's needs. Understanding the strengths and gaps of each broker helps developers make informed decisions.
Go through your code and follow the business logic. Whenever a question or doubt arises, there is potential for improvement. Your Code May Come back to You for Various Reasons The infrastructure, environment, or dependencies have evolved You want to reuse your code or logic in another context You need to introduce someone else or present your work before a wider audience The business requirements have changed Some improvements are needed There is a functional bug; etc. There are two, equally valid approaches here — either you fix the issue(s) with minimal effort and move on to the next task, or you take the chance to revisit what you have done, evaluate and possibly improve it, or even decide it is no longer needed, based on the experience and knowledge you have gained in the meantime. The big difference is that when you re-visit your code, you improve your skills as a side effect of doing your daily job. You may consider this a small investment that will pay for itself by increasing your efficiency in the future. A Few Examples Why did I do all this, where can I find the requirements? Developers often context switch between unrelated tasks — you can save time for onboarding yourself and others by maintaining better comments/documentation. A reference to a ticket could do the job, especially if there are multiple tickets. If possible, keep the requirements together with your code, otherwise try to summarize them. Hmm, this part is inefficient! In many cases this happens due to chasing deadlines, blindly copying code around, or not considering the real amount of data during development. You may find yourself retrieving the same data many times too. Writing efficient code always pays off by saving on iterations to improve performance. When you revisit your code, you may find that there are new and better ways to achieve the same goal. Oh, this is brittle — my assumptions may not hold in the future! "This will never happen" — you have heard it so many times at all levels of competence. No comment is needed here — a good reason why you should avoid writing brittle code is that you may want to reuse it in a different context. It's really hard to make no assumptions, but when you revisit your code, you should do your best to make as few assumptions as possible. Also consider that your code may run in different environments, where defaults and conventions may differ — never rely on things like date and number formats, order or completeness of data, availability of configuration or external services, etc. Oops, it is incomplete — it only covers a subset of the business requirements! You have no one to blame — this is your own code. Don't leave it incomplete, because it will come back to you and that always happens at the worst time possible. I'm lost following my own logic ... You definitely hit technical debt — and technical debt is immortal. As you develop professionally, you start doing things in more standard and widely recognized ways, so they are easier to maintain. It is quite tempting not to touch something that works. However, remember that, even if it works, it is only useable in the present context. Unreadable code is not reusable, not to mention it is hard to maintain. Fighting the technical debt pays by saving time and effort by allowing you to reuse code and logic. Uh, it's so big, it will take too much time to improve and I don't have enough time right now! Yet another type of technical debt. In a large and complex piece of code, some parts may appear unreachable in the actual context, making the code even less readable. This could be a problem, but nobody complained so far, so let's wait... Don't trust this line of thinking. The complaints will always come at the worst times. Summary Even when it isn't recognized by management or your peers, the effort of revisiting your own code makes you a better professional, which in turn gives you a better position on the market. Additionally, keeping your code clean and high-quality is satisfying, without the need for someone else's assessment — and being satisfied with your work is a good motivation to keep going. For myself, I would summarize all of the above in a single phrase — don't copy code but revisit it, especially if it's your own. It's like re-entering your new password when you change it — it can help you memorize it better, even if it's easier to copy and paste the same string twice. Nothing stops you from doing all this when developing new code too.
I recently read 6 Ways To Pass Parameters to Spring REST API. Though the title is a bit misleading, as it's unrelated to REST, it does an excellent job listing all ways to send parameters to a Spring application. I want to do the same for Apache APISIX; it's beneficial when you write a custom plugin. General Setup The general setup uses Docker Compose and static configuration. I'll have one plugin per way to pass parameters. YAML services: httpbin: image: kennethreitz/httpbin #1 apisix: image: apache/apisix:3.9.0-debian volumes: - ./apisix/conf/config.yml:/usr/local/apisix/conf/config.yaml:ro - ./apisix/conf/apisix.yml:/usr/local/apisix/conf/apisix.yaml:ro #2 - ./apisix/plugins:/opt/apisix/plugins:ro #3 ports: - "9080:9080" Local httpbin for more reliable results and less outbound network traffic Static configuration file Plugins folder, one file per plugin YAML deployment: role: data_plane role_data_plane: config_provider: yaml #1 apisix: extra_lua_path: /opt/?.lua #2 plugins: - proxy-rewrite #3 - path-variables #4 # ... Set static configuration Use every Lua file under /opt/apisix/plugins as a plugin Regular plugin Custom plugin, one per alternative Path Variables Path variables are a straightforward way to pass data. Their main issue is that they are limited to simple values, e.g., /links/{n}/{offset}. The naive approach is to write the following Lua code: Lua local core = require("apisix.core") function _M.access(_, ctx) local captures, _ = ngx.re.match(ctx.var.uri, '/path/(.*)/(.*)') --1-2 for k, v in pairs(captures) do core.log.warn('Order-Value pair: ', k, '=', v) end end APISIX stores the URI in ctx.var.uri Nginx offers a regular expression API Let's try: Shell curl localhost:9080/path/15/3 The log displays: Plain Text Order-Value pair: 0=/path/15/3 Order-Value pair: 1=15 Order-Value pair: 2=3 I didn't manage errors, though. Alternatively, we can rely on Apache APISIX features: a specific router. The default router, radixtree_host_uri, uses both the host and the URI to match requests. radixtree_uri_with_parameter lets go of the host part but also matches parameters. YAML apisix: extra_lua_path: /opt/?.lua router: http: radixtree_uri_with_parameter We need to update the route: YAML routes: - path-variables - uri: /path/:n/:offset #1 upstream_id: 1 plugins: path-variables: ~ Store n and offset in the context, under ctx.curr_req_matched We keep the plugin just to log the path variables: Lua function _M.access(_, ctx) core.log.warn('n: ', ctx.curr_req_matched.n, ', offset: ', ctx.curr_req_matched.offset) end The result is as expected with the same request as above: Plain Text n: 15, offset: 3 Query Parameters Query parameters are another regular way to pass data. Like path variables, you can only pass simple values, e.g., /?foo=bar. The Lua code doesn't require regular expressions: Lua local core = require("apisix.core") function _M.access(_, _) local args, _ = ngx.req.get_uri_args() for k, v in pairs(args) do core.log.warn('Order-Value pair: ', k, '=', v) end end Let's try: Shell curl localhost:9080/query\?foo=one\&bar=three The log displays: Plain Text Key-Value pair: bar=three Key-Value pair: foo=one Remember that query parameters have no order. Our code contains an issue, though. The ngx.req.get_uri_args() accepts parameters. Remember that the client can pass a query parameter multiple times with different values, e.g., ?foo=one&foo=two? The first parameter is the maximum number of values returned for a single query parameter. To avoid ignoring value, we should set it to 0, i.e., unbounded. Since every plugin designer must remember it, we can add the result to the context for other plugins down the chain. The updated code looks like this: Lua local core = require("apisix.core") function _M.get_uri_args(ctx) if not ctx then ctx = ngx.ctx.api_ctx end if not ctx.req_uri_args then local args, _ = ngx.req.get_uri_args(0) ctx.req_uri_args = args end return ctx.req_uri_args end function _M.access(_, ctx) for k, v in pairs(ctx.req_uri_args) do core.log.warn('Key-Value pair: ', k, '=', v) end end Request Headers Request headers are another way to pass parameters. While they generally only contain simple values, you can also use them to send structured values, e.g., JSON. Depending on your requirement, APISIX can list all request headers or a specific one. Here, I get all of them: Lua local core = require("apisix.core") function _M.access(_, _) local headers = core.request.headers() for k, v in pairs(headers) do core.log.warn('Key-Value pair: ', k, '=', v) end end We test with a simple request: Shell curl -H 'foo: 1' -H 'bar: two' localhost:9080/headers And we got more than we expected because curl added default headers: Plain Text Key-Value pair: user-agent=curl/8.4.0 Key-Value pair: bar=two Key-Value pair: foo=1 Key-Value pair: host=localhost:9080 Key-Value pair: accept=*/* Request Body Setting a request body is the usual way to send structured data, e.g, JSON. Nginx offers a simple API to collect such data. Lua local core = require("apisix.core") function _M.access(_, _) local args = core.request.get_post_args() --1 local body = next(args, nil) --2 core.log.warn('Body: ', body) end Access the body as a regular Lua table A table is necessary in case of multipart payloads, e.g., file uploads. Here, we assume there's a single arg, the content body. It's time to test: Shell curl localhost:9080/body -X POST -d '{ "foo": 1, "bar": { "baz": "two" } }' The result is as expected: JSON Body: { "foo": 1, "bar": { "baz": "two" } } Cookies Last but not least, we can send parameters via cookies. The difference with previous alternatives is that cookies persist on the client side, and the browser sends them with each request. On the Lua side, we need to know the cookie name instead of listing all query parameters or headers. Lua local core = require("apisix.core") function _M.access(_, ctx) local foo = ctx.var.cookie_foo --1 core.log.warn('Cookie value: ', foo) end The cookie is named foo and is case-insensitive Let's test: Shell curl --cookie "foo=Bar" localhost:9080/cookies The result is correct: Plain Text Cookie value: Bar Summary In this post, we listed five alternatives to pass parameters server-side and explained how to access them on Apache APISIX. Here's the API summary: Alternative Source API Path variable APISIX Router Use the radixtree_uri_with_parameter router Query parameter Nginx ngx.req.get_uri_args(0) Request header APISIX core lib core.request.headers() Request body APISIX core lib core.request.get_post_args() Cookie Method context parameter ctx.var.cookie_ Thanks a lot to Zeping Bai for his review and explanations. The complete source code for this post can be found on GitHub. To Go Further 6 Ways To Pass Parameters to Spring REST API How to Build an Apache APISIX Plugin From 0 to 1?
Here, I'd like to talk you through three Java katas, ranging from the simplest to the most complex. These exercises should help you gain experience working with JDK tools such as javac, java, and jar. By doing them, you'll get a good understanding of what goes on behind the scenes of your favorite IDE or build tools like Maven, Gradle, etc. None of this denies the benefits of an IDE. But to be truly skilled at your craft, understand your essential tools and don’t let them get rusty. - Gail Ollis, "Don’t hIDE Your Tools" Getting Started The source code can be found in the GitHub repository. All commands in the exercises below are executed inside a Docker container to avoid any particularities related to a specific environment. Thus, to get started, clone the repository and run the command below from its java-javac-kata folder: Shell docker run --rm -it --name java_kata -v .:/java-javac-kata --entrypoint /bin/bash maven:3.9.6-amazoncorretto-17-debian Kata 1: "Hello, World!" Warm Up In this kata, we will be dealing with a simple Java application without any third-party dependencies. Let's navigate to the /class-path-part/kata-one-hello-world-warm-up folder and have a look at the directory structure. Within this directory, we can see the Java project structure and two classes in the com.example.kata.one package. Compilation Shell javac -d ./target/classes $(find -name '*.java') The compiled Java classes should appear in the target/classes folder, as shown in the screenshot above. Try using the verbose option to see more details about the compilation process in the console output: Shell javac -verbose -d ./target/classes $(find -name '*.java') With that covered, let's jump into the execution part. Execution Shell java --class-path "./target/classes" com.example.kata.one.Main As a result, you should see Hello World! in your console. Try using different verbose:[class|gc|jni] options to get more details on the execution process: Shell java -verbose:class --class-path "./target/classes" com.example.kata.one.Main As an extra step, it's worth trying to remove classes or rename packages to see what happens during both the complication and execution stages. This will give you a better understanding of which problems result in particular errors. Packaging Building Jar Shell jar --create --file ./target/hello-world-warm-up.jar -C target/classes/ . The built jar is placed in the target folder. Don't forget to use the verbose option as well to see more details: Shell jar --verbose --create --file ./target/hello-world-warm-up.jar -C target/classes/ . You can view the structure of the built jar using the following command: Shell jar -tf ./target/hello-world-warm-up.jar With that, let's proceed to run it: Shell java --class-path "./target/hello-world-warm-up.jar" com.example.kata.one.Main Building Executable Jar To build an executable jar, the main-class must be specified: Shell jar --create --file ./target/hello-world-warm-up.jar --main-class=com.example.kata.one.Main -C target/classes/ . It can then be run via jar option: Shell java -jar ./target/hello-world-warm-up.jar Kata 2: Third-Party Dependency In this kata, you will follow the same steps as in the previous one. The main difference is that our Hello World! application uses guava-30.1-jre.jar as a third-party dependency. Also, remember to use the verbose option to get more details. So, without further ado, let's get to the /class-path-part/kata-two-third-party-dependency folder and check out the directory's structure. Compilation Shell javac --class-path "./lib/*" -d ./target/classes/ $(find -name '*.java') The class-path option is used to specify the path to the lib folder where our dependency is stored. Execution Shell java --class-path "./target/classes:./lib/*" com.example.kata.two.Main Packaging Building Jar Shell jar --create --file ./target/third-party-dependency.jar -C target/classes/ . And let us run it: Shell java --class-path "./target/third-party-dependency.jar:./lib/*" com.example.kata.two.Main Building Executable Jar Our first step here is to create a MANIFEST.FM file with the Class-Path specified: Shell echo 'Class-Path: ../lib/guava-30.1-jre.jar' > ./target/MANIFEST.FM Next up, we build a jar with the provided manifest option: Shell jar --create \ --file ./target/third-party-dependency.jar \ --main-class=com.example.kata.two.Main \ --manifest=./target/MANIFEST.FM \ -C target/classes/ . Finally, we execute it: Shell java -jar ./target/third-party-dependency.jar Building Fat Jar First of all, we need to unpack our guava-30.1-jre.jar into the ./target/classes/ folder (be patient, this can take some time): Shell cp lib/guava-30.1-jre.jar ./target/classes && \ cd ./target/classes && \ jar xf guava-30.1-jre.jar && \ rm ./guava-30.1-jre.jar && \ rm -r ./META-INF && \ cd ../../ With all the necessary classes in the ./target/classes folder, we can build our fat jar (again, be patient as this can take some time): Shell jar --create --file ./target/third-party-dependency-fat.jar --main-class=com.example.kata.two.Main -C target/classes/ . Now, we can run our built jar: Shell java -jar ./target/third-party-dependency-fat.jar Kata 3: Spring Boot Application Conquest In the /class-path-part/kata-three-spring-boot-app-conquest folder, you will find a Maven project for a simple Spring Boot application. The main goal here is to apply everything that we have learned so far to manage all its dependencies and run the application, including its test code. As a starting point, let's run the following command: Shell mvn clean package && \ find ./target/ -mindepth 1 ! -regex '^./target/lib\(/.*\)?' -delete This will leave only the source code and download all necessary dependencies into the ./target/lib folder. Compilation Shell javac --class-path "./target/lib/compile/*" -d ./target/classes/ $(find -P ./src/main/ -name '*.java') Execution Shell java --class-path "./target/classes:./target/lib/compile/*" com.example.kata.three.Main As an extra step for both complication and execution, you can try specifying all necessary dependencies explicitly in the class-path. This will help you understand that not all artifacts in the ./target/lib/compile are needed to do that. Packaging Let's package our compiled code as a jar and try to run it. It won't be a Spring Boot jar because Spring Boot uses a non-standard approach to build fat jars, including its own class loader. See the documentation on The Executable Jar Format for more details. In this exercise, we will package our source code as we did before to demonstrate that everything can work in the same way with Spring Boot, too. Shell jar --create --file ./target/spring-boot-app-conquest.jar -C target/classes/ . Now, let's run it to verify that it works: Shell java --class-path "./target/spring-boot-app-conquest.jar:./target/lib/compile/*" com.example.kata.three.Main Test Compilation Shell javac --class-path "./target/classes:./target/lib/test/*:./target/lib/compile/*" -d ./target/test-classes/ $(find -P ./src/test/ -name '*.java') Take notice that this time we are searching for source files in the ./src/test/ directory, and both the application source code and test dependencies are added to the class-path. Test Execution To be able to run code via java, we need an entry point (a class with the main method). Traditionally, tests are run via a Maven plugin or by an IDE, which have their own launchers to make this process comfortable for developers. To demonstrate test execution, the junit-platform-console-standalone dependency, which includes the org.junit.platform.console.ConsoleLauncher with the main method, is added to our pom.xml. Its artifact can also be seen in the ./target/lib/test/* folder. Shell java --class-path "./target/classes:./target/test-classes:./target/lib/compile/*:./target/lib/test/*" \ org.junit.platform.console.ConsoleLauncher execute --scan-classpath --disable-ansi-colors Wrapping Up Gail's article, "Don’t hIDE Your Tools" quoted at the very beginning of this article, taken from 97 Things Every Java Programmer Should Know by Kevlin Henney and Trisha Gee, inspired me to start thinking in this direction and eventually led to the creation of this post. Hopefully, by doing these katas and not just reading them, you have developed a better understanding of how the essential JDK tools work.
In any microservice, managing database interactions with precision is crucial for maintaining application performance and reliability. Usually, we will unravel weird issues with database connection during performance testing. Recently, a critical issue surfaced within the repository layer of a Spring microservice application, where improper exception handling led to unexpected failures and service disruptions during performance testing. This article delves into the specifics of the issue and also highlights the pivotal role of the @Transactional annotation, which remedied the issue. Spring microservice applications rely heavily on stable and efficient database interactions, often managed through the Java Persistence API (JPA). Properly managing database connections, particularly preventing connection leaks, is critical to ensuring these interactions do not negatively impact application performance. Issue Background During a recent round of performance testing, a critical issue emerged within one of our essential microservices, which was designated for sending client communications. This service began to experience repeated Gateway time-out errors. The underlying problem was rooted in our database operations at the repository layer. An investigation into these time-out errors revealed that a stored procedure was consistently failing. The failure was triggered by an invalid parameter passed to the procedure, which raised a business exception from the stored procedure. The repository layer did not handle this exception efficiently; it bubbled up. Below is the source code for the stored procedure call: Java public long createInboxMessage(String notifCode, String acctId, String userId, String s3KeyName, List<Notif> notifList, String attributes, String notifTitle, String notifSubject, String notifPreviewText, String contentType, boolean doNotDelete, boolean isLetter, String groupId) throws EDeliveryException { try { StoredProcedureQuery query = entityManager.createStoredProcedureQuery("p_create_notification"); DbUtility.setParameter(query, "v_notif_code", notifCode); DbUtility.setParameter(query, "v_user_uuid", userId); DbUtility.setNullParameter(query, "v_user_id", Integer.class); DbUtility.setParameter(query, "v_acct_id", acctId); DbUtility.setParameter(query, "v_message_url", s3KeyName); DbUtility.setParameter(query, "v_ecomm_attributes", attributes); DbUtility.setParameter(query, "v_notif_title", notifTitle); DbUtility.setParameter(query, "v_notif_subject", notifSubject); DbUtility.setParameter(query, "v_notif_preview_text", notifPreviewText); DbUtility.setParameter(query, "v_content_type", contentType); DbUtility.setParameter(query, "v_do_not_delete", doNotDelete); DbUtility.setParameter(query, "v_hard_copy_comm", isLetter); DbUtility.setParameter(query, "v_group_id", groupId); DbUtility.setOutParameter(query, "v_notif_id", BigInteger.class); query.execute(); BigInteger notifId = (BigInteger) query.getOutputParameterValue("v_notif_id"); return notifId.longValue(); } catch (PersistenceException ex) { logger.error("DbRepository::createInboxMessage - Error creating notification", ex); throw new EDeliveryException(ex.getMessage(), ex); } } Issue Analysis As illustrated in our scenario, when a stored procedure encountered an error, the resulting exception would propagate upward from the repository layer to the service layer and finally to the controller. This propagation was problematic, causing our API to respond with non-200 HTTP status codes—typically 500 or 400. Following several such incidents, the service container reached a point where it could no longer handle incoming requests, ultimately resulting in a 502 Gateway Timeout error. This critical state was reflected in our monitoring systems, with Kibana logs indicating the issue: `HikariPool-1 - Connection is not available, request timed out after 30000ms.` The issue was improper exception handling, as exceptions bubbled up through the system layers without being properly managed. This prevented the release of database connections back into the connection pool, leading to the depletion of available connections. Consequently, after exhausting all connections, the container was unable to process new requests, resulting in the error reported in the Kibana logs and a non-200 HTTP error. Resolution To resolve this issue, we could handle the exception gracefully and not bubble up further, letting JPA and Spring context release the connection to the pool. Another alternative is to use @Transactional annotation for the method. Below is the same method with annotation: Java @Transactional public long createInboxMessage(String notifCode, String acctId, String userId, String s3KeyName, List<Notif> notifList, String attributes, String notifTitle, String notifSubject, String notifPreviewText, String contentType, boolean doNotDelete, boolean isLetter, String groupId) throws EDeliveryException { ……… } The implementation of the method below demonstrates an approach to exception handling that prevents exceptions from propagating further up the stack by catching and logging them within the method itself: Java public long createInboxMessage(String notifCode, String acctId, String userId, String s3KeyName, List<Notif> notifList, String attributes, String notifTitle, String notifSubject, String notifPreviewText, String contentType, boolean doNotDelete, boolean isLetter, String loanGroupId) { try { ....... query.execute(); BigInteger notifId = (BigInteger) query.getOutputParameterValue("v_notif_id"); return notifId.longValue(); } catch (PersistenceException ex) { logger.error("DbRepository::createInboxMessage - Error creating notification", ex); } return -1; } With @Transactional The @Transactional annotation in Spring frameworks manages transaction boundaries. It begins a transaction when the annotated method starts and commits or rolls it back when the method completes. When an exception occurs, @Transactional ensures that the transaction is rolled back, which helps appropriately release database connections back to the connection pool. Without @Transactional If a repository method that calls a stored procedure is not annotated with @Transactional, Spring does not manage the transaction boundaries for that method. The transaction handling must be manually implemented if the stored procedure throws an exception. If not properly managed, this can result in the database connection not being closed and not being returned to the pool, leading to a connection leak. Best Practices Always use @Transactional when the method's operations should be executed within a transaction scope. This is especially important for operations involving stored procedures that can modify the database state. Ensure exception handling within the method includes proper transaction rollback and closing of any database connections, mainly when not using @Transactional. Conclusion Effective transaction management is pivotal in maintaining the health and performance of Spring Microservice applications using JPA. By employing the @Transactional annotation, we can safeguard against connection leaks and ensure that database interactions do not degrade application performance or stability. Adhering to these guidelines can enhance the reliability and efficiency of our Spring Microservices, providing stable and responsive services to the consuming applications or end users.
This article is part of a range of articles called “Mastering Object-Oriented Design Patterns.” The collection consists of four articles and aims to provide profound guidance on object-oriented design patterns. The articles address the introduction of the design patterns issues, their sources, and the advantages of their use. In addition, the tutorial series provides full explanations of the common design patterns. Every article starts with real-life analogies, discusses the pros and cons of each pattern, and provides a Java example implementation. Once you find the title, “Mastering Object-Oriented Design Patterns,” you can explore the whole series and master object-oriented design patterns. Once upon a time, there was a new notion called “design patterns” in software engineering. This concept has revolutionized how developers approach complex software design. Design patterns are verified solutions to frequently encountered problems. However, where did this idea originate, and how did it significantly contribute to object-oriented programming? Origin of Design Pattern Design patterns first appeared in architecture, not in software. An architect and design theorist, Christopher Alexander, introduced the idea in his influential work, “A Pattern Language: Towns, Buildings, Construction.” Alexander sought to develop a pattern language to solve some city spatial and communal problems. These patterns included several details, such as window heights and the organization of green zones within the neighborhoods. This way, it sets the ground for a design approach focusing on reusable solutions to the same problems. Captivated by the concept of Alexander, a group of four software engineers (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides), also known as the Gang of Four (GoF), recognized the potential of using this concept in software development. In 1994, they published “Design Patterns: Book “Elements of Reusable Object-Oriented” Software that translated the pattern language of architecture into the world of object-oriented programming (OOP). This seminal publication presented twenty-three design patterns targeted at addressing typical design issues. It soon became a best-seller and a vital tool in software engineering instruction. Introduction to Design Patterns What Are Design Patterns? Design patterns are not recipes but recommendations and tips for solving typical design problems. They are a pool of bright ideas and experiences of the software development community. These patterns assist the developers in building flexible, low-maintenance, and reusable code. Design patterns guide common language and methodology for solving design problems, simplifying collaboration among developers, and speeding up the development process. Picture-making software is like assembling a puzzle, except that you can continuously be given the same piece. Design patterns are your map indicating how you can fit those pieces every time. Design patterns are helpful techniques for resolving common coding issues. They can be understood as a set of coding challenge cookbooks. Rather than giving you ready-made code snippets, they present ways to solve particular problems in your projects. The purpose of design patterns is to reduce coding complexities, help you solve problems faster, and keep your code as flexible as possible for the future. Design Patterns vs. Algorithms Nevertheless, both provide solutions, but an algorithm is a sequence of steps to reach a goal, just like a cooking recipe. On the other hand, a design pattern is more of a template. It provides the layout and major components of the solution but does not specify building details; consequently, it is flexible in how this solution is being implemented in your project. Both algorithms and design patterns provide solutions. An algorithm is like a process, a recipe in the kitchen that makes you reach a target. Alternatively, a design pattern is like a blueprint. It gives the framework and the factor elements of the solution but lets you select the structure details, which makes it flexible for your project demands. Inside a Design Pattern A design pattern typically includes: A design pattern typically includes: Intent: What the pattern does and what it solves. Motivation: The reason and the way it can help. Structure of classes: A schematic indicating how its parts communicate. Code example: Commonly made available in popular programming languages to facilitate comprehension. Some will also address when to use the pattern, how to apply it, and its interaction with other patterns, leaving you with a complete toolset for more innovative coding. Why Use Design Patterns? Design patterns in coding are a kind of secret toolset. They make solving common problems easier, and here’s why embracing design patterns can be a game-changer: They make solving common issues more accessible and that’s why embracing design patterns can be a game-changer: Proven and ready-to-use solutions: Imagine owning a treasure chest of brilliant hacks already worked out by professional coders. That’s what design patterns are—several clever, immediately applicable, professional-quality solutions that allow you to solve problems quickly and correctly. Simplifying complexity: Any great software is minimalistic in a sense. Design patterns assist you in splitting large and daunting problems into small and manageable chunks, thus making your code neater and your life simpler. Big picture focus: Design patterns allow you to spend less time on code structure and more time on doing cool stuff. This will enable you to concentrate more on producing great features rather than struggling with the fundamentals. Common language: Design patterns provide the developers with a common language, so when you say, “Let’s use a Singleton here,” everyone gets it. This leads to more efficient work and less confusion. Reusability and maintainability: The design patterns encourage code reuse via inheritance and interfaces, which allows classes to be adaptable and systems easy to maintain. This method shortens development cycles and keeps systems fortified over time. Improved scalability and flexibility: The MVC pattern allows for a more defined separation of the different parts of your code, making your system more flexible and able to grow with little adjustments. Boosted readability and understandability: Properly implemented design patterns increase the readability and understandability of your code, making it easier for other people to understand and contribute without too much explanation. In a nutshell, design patterns are all about making coding more comfortable, efficient, and even entertaining. They enable you to work on extension rather than invention, which allows you to improve the software without reinventing the wheel. Navigating the Tricky Side of Design Patterns Design patterns are secret ingredients that make writing code more accessible and practical. But they are not ideal. Here are a couple of things to be aware of: Not suitable for every programming language: However, using a design pattern may sometimes not be appropriate for a specific language in a programming language. For instance, a complex pattern may be redundant if the language has a simple feature that can do the job. It is just like employing an intelligent instrument while a simple one is sufficient. Being too rigid with patterns: Although design patterns are derived from best practices, their strict adherence may cause undesirable behavior. It’s similar to sticking to a recipe so rigidly that you do not make it according to your taste. At times, you need to modify to suit the particular requirements of your project. Overusing patterns: It is pretty simple to lose control and believe that every problem can be addressed through a design pattern. Yet, not all problems need a pattern. It is akin to using a hammer for all tasks when, at times, a screwdriver is sufficient. Adding unnecessary complexity: Design patterns can also introduce complexity to your code. If not handled with care, they can complicate your project. How To Avoid the Pitfalls However, despite the troubles, design patterns are still quite helpful. The key is to use them wisely: Choose the appropriate tool for the task: Not all problems need a design pattern. Sometimes, simpler is better. Adapt and customize: Never be afraid to adjust a pattern to make it suit you better. Please keep it simple: Do not make your code more complicated by using patterns that are not required. In summary, design patterns are similar to spices in cooking: applied correctly, they can improve your dish (or project). Yet, it’s necessary to employ them in moderation and not let them overcome the food. Types of Design Patterns Design patterns are beneficial methods applied in software design. They facilitate code organization and management during the development and preservation of applications. Regard them as clever construction techniques and improvements to your software projects. Let’s quickly check out the three main types: Creational Patterns: Building Blocks Creational patterns are equivalent to picking up the suitable LEGO blocks to begin your model building. Their attention is directed to simplifying the process of creating objects or groups of objects. This way, you can build up the software flexibly and efficiently, as if picking out the LEGO pieces that fit your design. Structural Patterns: Putting It All Together Structural patterns are all about how you build your LEGO bricks. They help you arrange the pieces (or objects) into more significant structures, with everything neat and well-arranged. It is akin to following a LEGO manual to guarantee your spaceship or castle will be sturdy and neat. Behavioral Patterns: Making It Work LEGO behavioral patterns are just about making your LEGO creation do extraordinary things. For instance, think about making the wings of your LEGO spaceship move. In software, these patterns enable various program components to interact and cooperate, ensuring everything functions as intended. Design patterns could be as simple as idioms that only run in a programming language or as complicated as architectural patterns that shape the entire application. They are your tool in the tool kit, available during a small function and throughout the software’s structure. Comprehending these patterns is like learning the tricks of constructing the most incredible LEGO sets. They make you a software genius; all your coding will seem relaxed and fun! Conclusion Our first module is finally over. It has been a fantastic trip into the principles behind design patterns and how the patterns are leveraged in software engineering. We found it fascinating to understand the concept of design patterns and their role in software engineering. Design patterns are not merely coding shortcuts but crystallized wisdom that provides reusable solutions for typical design issues. They simplify the object-oriented programming process and make it work faster, thus creating cleaner codes. On the other hand, they are not simple. We have pointed out that it is essential to know when and how to use them appropriately. In closing this chapter, we invite you to browse the other parts of the “Mastering Object-Oriented Design Patterns” series. Each part reinforces your comprehension and skill, making you more confident when applying design patterns to your projects. If you want to develop your architectural skills, speed up your development process, or improve the quality of your code, this series is here to help you. References Design Patterns Head First Design
May 8, 2024 by
Getting Started With OCR docTR on Ubuntu
May 8, 2024 by
Explainable AI: Making the Black Box Transparent
May 16, 2023 by CORE
May 8, 2024 by
Getting Started With OCR docTR on Ubuntu
May 8, 2024 by
Low Code vs. Traditional Development: A Comprehensive Comparison
May 16, 2023 by
Getting Started With OCR docTR on Ubuntu
May 8, 2024 by
Going with the Flow for CI/CD: Heroku Flow With Gitflow
May 8, 2024 by CORE
May 8, 2024 by
Going with the Flow for CI/CD: Heroku Flow With Gitflow
May 8, 2024 by CORE
Low Code vs. Traditional Development: A Comprehensive Comparison
May 16, 2023 by