Project Information
|
Note: Development on Java on Contracts has been suspended for now, as I'm currently working on a successor for C4J. There are so many DBC libraries for Java out there, yet none of them that I've looked at met the following criteria:
See Installation for instructions on how to use JOC. FeaturesImplemented in Version 0.3Pre-Conditions bound to Method ParametersThe pre-condition will be checked before the method is executed. Subclasses need to explicitly define the pre-condition again, as they are allowed to accept a wider range of values. @JOCEnabled public class CashTerminal { public void withdraw(@GreaterThanLong(0) long amount) { // withdraw money from customer's bank account } } Invariants bound to FieldsThe invariants will be checked before and after a method of the class or subclasses is executed. For constructors, it will only be checked after a constructor of this class has been executed. This is sufficient, as subclasses are required to call a constructor of their superclass anyway. @JOCEnabled public class Account { @NotNull private Customer customer; } Post-ConditionsFor simple post-conditions, the contract annotations check the behavior of the return value of the method. @JOCEnabled public class Customer { @ContainsString("Welcome") public String getWelcomeMessage() { // generate Welcome Message for Customer } } Writing own ContractsOwn contracts can be written very easily. For example, this is the complete implementation of the @GreaterThanLong contract. @Documented @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) public @interface GreaterThanLong { long value(); } public class GreaterThanLongContract extends Contract<GreaterThanLong> { @ForFieldEvaluateAtBeginningOfAllMethods @ForFieldEvaluateAtEndOfAllBehaviors @ForParameterEvaluateAtBeginningOfBehavior @ForMethodEvaluateAtEndOfMethodAndInherit public PartialEvaluationResult checkContract(GreaterThanLong annotation, long value) { return getResult(value > annotation.value(), "Value " + value + " is not greater than " + annotation.value() + "."); } } Of course, you're also encouraged to write a respective Unit Test and System Test. Custom contracts need to be added to the configuration in order for JOC to be able to detect them. contract_joc.example.CustomContract = On Pure methodsMethods being annotated with @Pure must not have any side effects, which means that neither the object's state itself nor any other object's state is being modified. This is implemented as follows:
Also, custom contract methods are implicitly @Pure to ensure that using contracts won't have any side effects, with the exception of write access to their own fields. Custom Post-ConditionsAs post-conditions often heavily depend on the method they're used in, you might have to write your own post-conditions. A nice and refactoring-safe way of doing so without specifying new contracts is to give each post-condition its own class. @JOCEnabled public class CashTerminal { protected Account account; @PostCondition(PostWithdraw.class) public boolean withdraw(int amount) { // withdrawing amount from the account, returns true on success } protected static class PostWithdraw { private int initialBalance; @BeforeCondition // optional and only needed if the post-condition needs values from the beginning of the method call protected void before(CashTerminal target, int amount) { initialBalance = target.account.getBalance(); } @Condition // needs to return false on contract violation protected boolean after(CashTerminal target, boolean returnValue, int amount) { if (returnValue) { return initialBalance == (target.account.getBalance() + amount); } return initialBalance == target.account.getBalance(); } } } The methods annotated with @BeforeCondition and @Condition get the object and the parameters of the method at the beginning and the end of the method execution respectively. To reduce code overhead and to ease refactoring, you can ignore parameters at the end of the parameter list, so in the example above, the parameter amount in method before could've been left out. @Condition also gets the return value as the second parameter, if the annotated method is returning a value. Note that you should declare all fields, inner classes being used as post-conditions and their methods as protected, as potential subclasses overriding the method are implicitly using the same post-conditions. Although target could be replaced with CashTerminal.this and the method parameter could be removed completely, using the method parameter is being enforced to keep the same pattern when moving post-conditions to external classes and thus to ease refactoring, as shown below. Custom Post-Conditions as external classesOf course, you don't have to use an inner class for post-condition-classes and can use a normal class just as well. This is especially useful if you need the same post condition in different classes. Access to class members should then be defined with interfaces, as is shown in the following example. public interface Withdrawable { public Account getAccount(); public void withdraw(int amount); } @JOCEnabled public class CashTerminal implements Withdrawable { private Account account; public Account getAccount() { return account; } @PostCondition(PostWithdraw.class) public boolean withdraw(int amount) { // withdrawing amount from the account, returns true on success } } public class PostWithdraw { private int initialBalance; @BeforeCondition public void before(Withdrawable target) { initialBalance = target.getAccount().getBalance(); } @Condition protected boolean after(CashTerminal target, boolean returnValue, int amount) { if (returnValue) { return initialBalance == (target.account.getBalance() + amount); } return initialBalance == target.account.getBalance(); } // there can also be multiple methods being annotated with @BeforeCondition and @Condition if there is more than one condition to be met } You could even move the @PostCondition annotation to the interface, if all implementing classes have to obey the PostWithdraw post-condition. Custom Pre-Conditions and InvariantsIf you need more specific pre-conditions and invariants than the ones provided by pre-conditions bound to method parameters and invariants bound to fields, you can arbitrarily define those just as you can define custom post-conditions. All features shown above are also available with these. @JOCEnabled @Invariant(CashTerminal.BalanceGreaterEqualZero.class) public class CashTerminal { protected Account account; @PreCondition(PreWithdraw.class) public void withdraw(int amount) { // withdrawing amount from the account } protected static class PreWithdraw { @Condition protected boolean before(CashTerminal target, int amount) { return amount > target.account.getBalance(); } } protected static class BalanceGreaterEqualZero { protected boolean check(CashTerminal target) { return target.account.getBalance() >= 0; } } } Conditions retaining state with HooksSometimes it is necessary to retain a state in between method calls, for example if you want to ensure that method A is called before method B. Hooks allow you to do just that. @JOCEnabled public class CashTerminal { protected Account account; @PreConditionHook(LoginBeforeWithdraw.class) public void loginAccount(Account account) { // logging in the given account } @PreCondition(LoginBeforeWithdraw.class) public void withdraw(int amount) { // withdrawing amount from the account, needs the account } protected static class LoginBeforeWithdraw { private boolean isLoginAccountCalled = false; @AfterHook // being executed after the method annotated with @PreConditionHook protected void afterLoginAccount(CashTerminal target) { isLoginAccountCalled = true; } @Condition protected boolean condition(CashTerminal target) { return isLoginAccountCalled; } } } Profiling ContractsIf you have time-critical methods and want to track if a method execution exceeds a defined amount of time, you can use the @Profile contract. public class CashTerminal { @Profile(1000) public boolean authenticate(LoginCredentials credentials) { // calling the authentication service might take some time... } } The value provided defines the amount of milliseconds before a contract violation is thrown at the end of the method. You can also set the value to 0, which is only useful if you want to log the execution time for every method call using LogOn in the action configuration as explained below. If you need greater precision, you can set the nano attribute of the @Profile annotation to true and the value will be interpreted in nanoseconds. Enforce correctly overriding equals and hashCodeOne of the most misunderstood contracts is the one of Object.equals. As such, it makes perfect sense to write an @Equals contract in order to enforce correctly overriding equals and hashCode. The @Equals contract implemented in JOC executes the following runtime-checks, whenever equals is being called:
The only condition of Object.equals which is not implemented is transitiveness, as it would require at least 3 different objects, which would only be available when caching the runtime results. Define contracts for foreign classesSo far, all contracts could only be directly assigned to your own classes. With JOC, you're also able to inject contracts into existing classes. For example, if you want to avoid using a problematic class in a library being used in your project, you can simply @Deny calling the constructors. @JOCExternalContract(ProblematicClass.class) public class ProblematicClassContract { @Deny public ProblematicClassContract() { } } In order for JOC to find the external contract, you need to include it in the config. external_joc.example.ProblematicClassContract = On Matching between the contract class and the class itself is done as follows:
Note that post-condition and invariant contract inheritance is only done for subclasses who're also defining an external contract - even if it's empty. General ConfigurationSome general configuration parameters can be specified in joc.properties:
enable_package.name.to.be.enabled = On
_implicit-equals = On
_implicit-equals-pure = On _implicit-hashcode-pure = On Action ConfigurationBy default, once a contract is broken, JOC will throw an AssertionError and your application will most likely terminate. You can arbitrarily change this behavior for the whole codebase or specific packages, classes, methods, fields and specific contracts. The behavior can be configured using the following commands:
# affects all contracts to be ignored, note that subsequent properties take precedence action_* = AssertOff # affects contracts in package joc.example and all its subpackages to silently log broken contracts action_joc.example.* = LogOn # affects all @NotNull contracts in package joc.example and all its subpackages to throw AssertionErrors without logging action_joc.example.*@joc.contract.NotNull = AssertOn, LogOff Implemented on trunkGetters and SettersWriting a conventional contract for a Setter can be a difficult task. At most, it includes the following:
The @Setter contract takes care of all of the above and can be used for any method that must behave as a Setter. The @Getter contract works in a similar fashion and makes sure, that the corresponding field's value is being returned. @JOCEnabled public class Account { private int balance; @Getter public int getBalance() { return balance; } @Setter public void setBalance(int balance) { this.balance = balance; } } If you want to implicitly annotate all getters and setters with the @Getter and @Setter annotations, you can do so in the global configuration. _implicit-getter = On _implicit-setter = On Method Call SequencesFor some classes, it is important in which order the methods are being called. To enforce the call order on the class and all its subclasses, you can define a JOC call sequence by using the JOCCallSequence annotation. @JOCEnabled @JOCCallSequence(CashTerminalCallSequenceProvider.class) public class CashTerminal { public void loginAccount(Account account) { /* impl */ } public boolean authenticate(LoginCredentials credentials) { /* impl */ } public void withdraw() { /* impl */ } public void logout() { /* impl */ } protected static class CashTerminalCallSequenceProvider extends CallSequenceProvider<CashTerminal> { @Override public void callSequence(CashTerminal target) { call().loginAccount(null); before().withdraw(); before().authenticate(null); before().logout(); } } } The method callSequence in the CallSequence-class will be called by JOC when loading the class in order to make the proper code adjustments. Using the methods call and before, an arbitrary amount of call sequences can be defined. A call sequence starts by calling call and is followed by calling before for each following method in the call sequence. The parameters for the methods being called in a call sequence are being disregarded. Thus, you can just use null for reference types and 0 or false for the respective primitive types. Defining call sequences also works for interfaces and abstract classes, and all inherited classes must obey the defined call sequences in the class hierarchy. ProposedMore General Configuration
_contract-violation-throwable = package.with.CustomThrowable Scope ConfigurationThe scope configuration allows you to specify, in which context contracts will be evaluated. As seen in the example above, it makes no sense to @Deny calling all of the Integer constructors, as you could just as well @Deny the whole class itself. This is where the scope configuration comes in, allowing you to use certain contracts depending on the uppermost element of the call stack. The Integer example above could hence be completed with the following scope configuration, rejecting to use the @Deny contract for all invocations from java.lang. joc.scope.java.lang.Integer.*@Deny = Reject=java.lang.* Auto-Profiling JOCIf you need to know how much JOC is slowing your code execution down, you can turn on auto-profiling in the configuration. JOC will then measure the time it takes to execute the production code versus the time it takes to bind contracts and evaluate them. This allows you to analyze which contracts you might want to turn off in production and where potential bottlenecks are. The following configuration turns auto-profiling on and logs the complete results every 10 seconds. You can also specify a frequency of 0 seconds, in which case auto-profiling is still turned on, but you need to call joc.Profiling.logResults() to log the results at a specific point in time. You can also call joc.Profiling.getResults() to further analyze the results in your code. joc.profiling = 10000 Method Call LoggingAutomated Specification TestingOne of the problems with DbC compared to TDD is that you don't have automated tests that can be run every time you integrate your changes into a central repository or even every time you hit Ctrl+S. The root of this problem is that you don't have any test data to begin with. JOC allows you to provide test data that can be used to do automated specification testing. Basically, if a method is called and all pre-conditions and invariants are fulfilled in the beginning of a method call, the post-conditions and invariants must also be fulfilled at the end of a method call. In order to provide JOC with the necessary test data, you can include a method in your test-class that is annotated with @JOCTest. In this method, you must provide instances of the class under test as well as instances of objects that can be used as method parameters. @JOCEnabled public class CashTerminal { protected Account account; /* constructor, etc. */ @PostCondition(/* a post-condition */) public boolean withdraw(@GreaterThanLong(0) int amount) { // withdrawing amount from the account, returns true on success } @PostCondition(/* another post-condition */) public void loginAccount(Account account) { // logging in the given account } @PostCondition(/* another post-condition */) public void loginAdminAccount(AdminAccount account) { // logging in the given account } } public class CashTerminalTest { @JOCTest public void jocTest(JOCTestEnvironment<CashTerminal> env) { CashTerminal terminal = new CashTerminal(); /* initialize terminal for specification testing */ env.addTarget(terminal); /* add more target objects */ env.addParameter(new Account("bobby", "1234")); env.addParameter(new Account("alice", "5678")); env.onlyFor().loginAccount(null); env.addParameter(new AdminAccount("admin", "1337")); } } |