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.