0

First, about the problem I try to solve.

Imagine there is an EntityPersistentAdapter which works with some abstraction over SQL (querydsl is used in my case). This adapter implements many CRUD operations related to entity, operations are split into many interfaces (i.e. DeleteEntityDataPort), but implemented by a single adapter class since they have something in common (usually the same predicates on where-clause among different operations). For one operation I would like to have a dry-run implementation to evaluate how much data will be removed and report back to user before actual removal. I could use a transaction and rollback on dry-run, but decided to implement it in even more safer manner by executing select count instead of delete + rollback. To execute select count I need exactly the same set of where-conditions which I use for delete, so I extracted those conditions to a separate private method. To not expose this implementation detail method and reuse it at the same time, I decided to use inner class bean (or nested + outer class injection).

Let's look at the code. I composed an example which will be easy to reproduce with fresh start.spring.io project:

// DeleteEntityDataPort.java
public interface DeleteEntityDataPort {
    long deleteDataForEntity(long entId);
}
// EntityPersistentAdapter.java

@Repository
public class EntityPersistentAdapter implements DeleteEntityDataPort {// actually implements many more fine-graned interfaces

    private final Sql sql;

    public EntityPersistentAdapter(Sql sql) {
        this.sql = Objects.requireNonNull(sql);
    }

    @Override
    public long deleteDataForEntity(long id) {
        return sql.delete(commonConditionForBothDeleteAndDryRunSelectCount());
    }

    private int commonConditionForBothDeleteAndDryRunSelectCount() {
        return 42;
    }

    // ....

    @Repository
    public /*static*/ class DryRunDeleteEntityData implements DeleteEntityDataPort {
//        final EntityPersistentAdapter outerClassInstance;
//
//        DryRunDeleteEntityData(EntityPersistentAdapter topLevel) {
//            this.outerClassInstance = topLevel;
//        }
//
//        @Override
//        public long deleteDataForEntity(long entId) {
//            return outerClassInstance.sql.recordsCountSelect(
//                    outerClassInstance.commonConditionForBothDeleteAndDryRunSelectCount()
//            );
//        }

        @Override
        public long deleteDataForEntity(long entId) {
            return sql.recordsCountSelect(
                    commonConditionForBothDeleteAndDryRunSelectCount()
            );
        }
    }
}
// Sql.java
/* Sql abstaction stub
 * @param condition represented by integer in both cases for simplicity, it does not matter
 */
@Component
public class Sql {

    private static final long RECORDS_AFFECTED_HARDCODED_STUB = 42;

    public long delete(int condition) {
        System.out.println("Dangerous sql delete by condition " + condition + " executed, returning number of affected records");
        return RECORDS_AFFECTED_HARDCODED_STUB;
    }

    public long recordsCountSelect(int condition) {
        System.out.println("Safe select count by condition " + condition + " executed");
        return RECORDS_AFFECTED_HARDCODED_STUB;
    }
}
// test
@SpringBootTest
class NestedJavaBeanApplicationTests {

    @Autowired
    private EntityPersistentAdapter.DryRunDeleteEntityData dryRunComponent;

    @Test
    void contextLoads() {
        dryRunComponent.deleteDataForEntity(1);
    }
}

Notice the commented code for turning inner class into static nested one. The NPE problem is the same in both cases.

java.lang.NullPointerException: Cannot invoke "com.example.nestedjavabean.beans.Sql.recordsCountSelect(int)" because "this.this$0.sql" is null

    at com.example.nestedjavabean.beans.EntityPersistentAdapter$DryRunDeleteEntityData.deleteDataForEntity(EntityPersistentAdapter.java:42)

The error I get is a NullPointerException on attempt to get outer class property (sql) from inner/nested class. That's because inner/nested class has access not to an instance of EntityPersistentAdapter, but to it's proxy. The problem only exists when my bean needs to be proxied (i.e. if I change @Repository to @Component on outer bean class the problem goes away, but in my real code I need my beans to be proxied).

Is there a way to solve the NPE problem without changing the structure of my code? Somehow force-inject non-proxy bean into nested class? If not, what would be the best alternative to achieve my goal? (reuse private condition method and do not expose implementation details)

Another option I thought of was to abstract away commonConditionForBothDeleteAndDryRunSelectCount and related code to a base class with protected visibility and then make my implementation classes to inherit it. But I wanted to avoid inheritance, since I do not see a clear 'is-a' relationship here.

You may also ask me why I try to use nested class to access private method instead of making a conditions method package-private. The answer is: I actually use Kotlin which does not have package-private visibility modifier.

Kirill
  • 6,762
  • 4
  • 51
  • 81
  • 2
    No there isn't. As a private method will not have access to the variables of the proxied instance. Your only solution is to use composition and to really wrap one in another (so you basically write an delegate to enhance your actual instance instead of using inner classes). So basically passing an instance of a `DeleteEntityDataPort` to the `DryRunEntityPersistentAdapter` instead of the inner class which tries to get the outer class. Else it simply won't be possible as the language doesn't allow it. – M. Deinum Jun 07 '21 at 12:00
  • If you're using Kotlin, why are you clinging to the sucky `package private` Java construct? Refactor the rest of your code to do it the kotlin way, instead of the package private way, and abstract it away. – Shark Jun 07 '21 at 13:14
  • @Shark, I just said package-private is not an option, I do not cling to it. What do you mean by Kotlin-way btw? To put all db persistence related code to a separate module and turn `private` method to `internal`? – Kirill Jun 07 '21 at 13:22
  • sorry then, i misunderstood. – Shark Jun 07 '21 at 13:26

0 Answers0