diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,3 +1,38 @@
 # jvm-streaming
 
 Expose Java iterators as streams from the streaming package.
+
+## Using it as a dependency
+
+Add `jvm-streaming` to the list of dependencies in your .cabal file.
+Then edit the `Setup.hs` file to add some necessary jars to the
+classpath.
+
+```Haskell
+import Distribution.Simple
+import Language.Java.Inline.Cabal
+import qualified Language.Java.Streaming.Jars
+
+main = do
+    jars <- Language.Java.Streaming.Jars.getJars
+    defaultMainWithHooks (addJarsToClasspath jars simpleUserHooks)
+```
+
+Add a `custom-setup` stanza to your .cabal file.
+
+```
+custom-setup
+  setup-depends:
+    base,
+    Cabal >= 1.24,
+    inline-java,
+    jvm-streaming
+```
+
+## Layout of source directories
+
+This is a multi-language package. We use
+Maven's [standard directory layout][maven-sdl] to organize source code
+in multiple languages side-by-side.
+
+[maven-sdl]: https://maven.apache.org/guides/introduction/introduction-to-the-standard-directory-layout.html
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,7 @@
+import Distribution.Simple
+import Language.Java.Inline.Cabal
+import qualified Language.Java.Batching.Jars
+
+main = do
+    jars <- Language.Java.Batching.Jars.getJars
+    defaultMainWithHooks (addJarsToClasspath jars simpleUserHooks)
diff --git a/benchmarks/Main.hs b/benchmarks/Main.hs
new file mode 100644
--- /dev/null
+++ b/benchmarks/Main.hs
@@ -0,0 +1,50 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Main where
+
+import Criterion.Main as Criterion
+import Control.DeepSeq
+import Data.Int
+import qualified Data.Vector.Storable as VS
+import Language.Java
+import Language.Java.Streaming ()
+import Streaming (Of, Stream)
+import qualified Streaming.Prelude as Streaming
+
+roundtrip :: forall a. (Reify a, Reflect a) => a -> IO ()
+roundtrip a = pushWithSizeHint 30 $ (reflect a >>= reify :: IO a) >> pop
+
+intValues :: [Int32]
+intValues = [1..4000]
+
+vValues :: [VS.Vector Int32]
+vValues = [ VS.fromList [i, i] | i <- [1..2000] ]
+
+benchBatching :: Benchmark
+benchBatching = deepseq intValues $
+    bgroup "roundtrips"
+    [ bgroup "ints"
+      [ bgroup "no batching"
+        [ bench "list" $ nfIO $ roundtrip intValues
+        ]
+      , bgroup "batching"
+        [ bench "streams" $ nfIO $ roundtrip
+            (Streaming.each intValues :: Stream (Of Int32) IO ())
+        ]
+      ]
+    , bgroup "int vectors"
+      [ bgroup "no batching"
+        [ bench "list" $ nfIO $ roundtrip vValues
+        ]
+      , bgroup "batching"
+        [ bench "streams" $ nfIO $ roundtrip
+            (Streaming.each vValues :: Stream (Of (VS.Vector Int32)) IO ())
+        ]
+      ]
+    ]
+
+main :: IO ()
+main =
+    withJVM ["-Djava.class.path=../jvm-batching/build/libs/jvm-batching.jar"] $
+      Criterion.defaultMain [benchBatching]
diff --git a/jvm-streaming.cabal b/jvm-streaming.cabal
--- a/jvm-streaming.cabal
+++ b/jvm-streaming.cabal
@@ -1,5 +1,5 @@
 name:                jvm-streaming
-version:             0.2.2
+version:             0.3
 synopsis:            Expose Java iterators as streams from the streaming package.
 description:         Please see README.md.
 homepage:            http://github.com/tweag/inline-java/tree/master/jvm-streaming#readme
@@ -9,7 +9,7 @@
 maintainer:          m@tweag.io
 copyright:           2015-2016 EURL Tweag.
 category:            FFI, JVM, Java
-build-type:          Simple
+build-type:          Custom
 cabal-version:       >=1.10
 extra-source-files:  README.md
 
@@ -18,24 +18,34 @@
   location: https://github.com/tweag/inline-java
   subdir: jvm-streaming
 
+custom-setup
+  setup-depends:
+    base,
+    Cabal >= 1.24,
+    inline-java >= 0.6.3,
+    jvm-batching >= 0.1 && <0.2
+
 library
-  hs-source-dirs: src
+  hs-source-dirs: src/main/haskell src/cabal/haskell
   default-language: Haskell2010
   exposed-modules:
     Language.Java.Streaming
+    Language.Java.Streaming.Jars
   build-depends:
     base >= 4.7 && < 5,
     distributed-closure >= 0.3,
-    jni >= 0.4.0 && <0.6,
+    jni >= 0.4.0 && <0.7,
     jvm >= 0.4.0 && <0.5,
-    inline-java >= 0.7 && <0.8,
+    jvm-batching >= 0.1 && <0.2,
+    inline-java >= 0.8 && <0.9,
     singletons >= 2.2,
-    streaming >= 0.1.4
+    streaming >= 0.1.4,
+    vector
 
 test-suite spec
   type:
     exitcode-stdio-1.0
-  hs-source-dirs: tests
+  hs-source-dirs: src/test/haskell
   main-is: Main.hs
   other-modules:
     Language.Java.StreamingSpec
@@ -48,3 +58,18 @@
     jvm-streaming,
     streaming
   default-language: Haskell2010
+
+benchmark micro-benchmarks
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs: benchmarks
+  build-depends:
+    base >=4.8 && <5,
+    criterion,
+    deepseq,
+    jvm,
+    jvm-streaming,
+    streaming,
+    vector
+  default-language: Haskell2010
+  ghc-options: -threaded
diff --git a/src/Language/Java/Streaming.hs b/src/Language/Java/Streaming.hs
deleted file mode 100644
--- a/src/Language/Java/Streaming.hs
+++ /dev/null
@@ -1,158 +0,0 @@
--- | Expose Java iterators as streams from the
--- <http://hackage.haskell.org/package/streaming streaming> package.
-
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE ExistentialQuantification #-}
-{-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE QuasiQuotes #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TemplateHaskell #-}
-{-# LANGUAGE TypeFamilies #-}
-{-# LANGUAGE RecordWildCards #-}
-{-# LANGUAGE UndecidableInstances #-}
-
-{-# OPTIONS_GHC -fno-warn-orphans #-}
-{-# OPTIONS_GHC -fno-warn-unused-top-binds #-}
-{-# OPTIONS_GHC -fplugin=Language.Java.Inline.Plugin #-}
-
-module Language.Java.Streaming () where
-
-import Control.Distributed.Closure.TH
-import qualified Data.Coerce as Coerce
-import Data.IORef (IORef, newIORef, readIORef, writeIORef)
-import Data.Int (Int64)
-import Data.Singletons (SomeSing(..))
-import Data.Word (Word8)
-import Foreign.Ptr (FunPtr, Ptr, freeHaskellFunPtr, intPtrToPtr, ptrToIntPtr)
-import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr)
-import qualified Foreign.JNI as JNI
-import Foreign.JNI.Types (jnull)
-import GHC.Stable
-  ( castPtrToStablePtr
-  , castStablePtrToPtr
-  , deRefStablePtr
-  , freeStablePtr
-  , newStablePtr
-  )
-import Language.Java
-import Language.Java.Inline
-import Streaming (Stream, Of)
-import qualified Streaming.Prelude as Streaming
-
-isPoppableStream :: IORef (Stream (Of a) IO ()) -> IO Word8
-isPoppableStream ref = do
-    readIORef ref >>= Streaming.uncons >>= \case
-      Nothing -> return $ fromIntegral $ fromEnum False
-      Just (x, stream) -> do
-        writeIORef ref (Streaming.cons x stream)
-        return $ fromIntegral $ fromEnum True
-
-popStream :: Reflect a => IORef (Stream (Of a) IO ()) -> IO (J (Interp a))
-popStream ref = do
-    stream <- readIORef ref
-    Streaming.uncons stream >>= \case
-      Nothing -> do
-        exc :: J ('Class "java.util.NoSuchElementException") <- new []
-        JNI.throw exc
-        return jnull
-      Just (x, stream') -> do
-        writeIORef ref stream'
-        reflect x
-
-type JNIFun a = JNIEnv -> Ptr JObject -> IO a
-
-foreign import ccall "wrapper" wrapObjectFun
-  :: JNIFun (Ptr (J ty)) -> IO (FunPtr (JNIFun (Ptr (J ty))))
-foreign import ccall "wrapper" wrapBooleanFun
-  :: JNIFun Word8 -> IO (FunPtr (JNIFun Word8))
-
--- Export only to get a FunPtr.
-foreign export ccall "jvm_streaming_freeIterator" freeIterator
-  :: JNIEnv -> Ptr JObject -> Int64 -> IO ()
-foreign import ccall "&jvm_streaming_freeIterator" freeIteratorPtr
-  :: FunPtr (JNIEnv -> Ptr JObject -> Int64 -> IO ())
-
-data FunPtrTable = forall ty. FunPtrTable
-  { hasNextPtr :: FunPtr (JNIFun Word8)
-  , nextPtr :: FunPtr (JNIFun (Ptr (J ty)))
-  }
-
-freeIterator :: JNIEnv -> Ptr JObject -> Int64 -> IO ()
-freeIterator _ _ ptr = do
-    let sptr = castPtrToStablePtr $ intPtrToPtr $ fromIntegral ptr
-    FunPtrTable{..} <- deRefStablePtr sptr
-    freeHaskellFunPtr hasNextPtr
-    freeHaskellFunPtr nextPtr
-    freeStablePtr sptr
-
-newIterator
-  :: Reflect a
-  => Stream (Of a) IO ()
-  -> IO (J ('Iface "java.util.Iterator"))
-newIterator stream = do
-    ref <- newIORef stream
-    hasNextPtr <- wrapBooleanFun $ \_ _ -> isPoppableStream ref
-    nextPtr <- wrapObjectFun $ \_ _ ->
-      -- Conversion is safe, because result is always a reflected object.
-      unsafeForeignPtrToPtr <$> Coerce.coerce <$> popStream ref
-    -- Keep FunPtr's in a table that can be referenced from the Java side, so
-    -- that they can be freed.
-    tblPtr :: Int64 <- fromIntegral . ptrToIntPtr . castStablePtrToPtr <$> newStablePtr FunPtrTable{..}
-    iterator <-
-      [java| new java.util.Iterator() {
-                @Override
-                public native boolean hasNext();
-
-                @Override
-                public native Object next();
-
-                @Override
-                public void remove() {
-                    throw new UnsupportedOperationException();
-                }
-
-                private native void hsFinalize(long tblPtr);
-
-                @Override
-                public void finalize() {
-                    hsFinalize($tblPtr);
-                }
-             } |]
-    klass <- JNI.getObjectClass iterator
-    JNI.registerNatives klass
-      [ JNI.JNINativeMethod
-          "hasNext"
-          (methodSignature [] (sing :: Sing ('Prim "boolean")))
-          hasNextPtr
-      , JNI.JNINativeMethod
-          "next"
-          (methodSignature [] (sing :: Sing ('Class "java.lang.Object")))
-          nextPtr
-      , JNI.JNINativeMethod
-          "hsFinalize"
-          (methodSignature [SomeSing (sing :: Sing ('Prim "long"))] (sing :: Sing 'Void))
-          freeIteratorPtr
-      ]
-    return iterator
-
-withStatic [d|
-  instance Interpretation (Stream (Of a) m r) where
-    type Interp (Stream (Of a) m r) = 'Iface "java.util.Iterator"
-
-  instance Reify a => Reify (Stream (Of a) IO ()) where
-    reify itLocal = do
-      -- We make sure the iterator remains valid while we reference it.
-      it <- JNI.newGlobalRef itLocal
-      return $ Streaming.untilRight $ do
-        call it "hasNext" [] >>= \case
-          False -> return (Right ())
-          True -> do
-            obj <- call it "next" []
-            Left <$> reify (unsafeCast (obj :: JObject))
-
-  instance Reflect a => Reflect (Stream (Of a) IO ()) where
-    reflect x = newIterator x
-  |]
diff --git a/src/cabal/haskell/Language/Java/Streaming/Jars.hs b/src/cabal/haskell/Language/Java/Streaming/Jars.hs
new file mode 100644
--- /dev/null
+++ b/src/cabal/haskell/Language/Java/Streaming/Jars.hs
@@ -0,0 +1,4 @@
+-- | Provides the paths to the jars needed to build with jvm-streaming.
+module Language.Java.Streaming.Jars (getJars) where
+
+import Language.Java.Batching.Jars
diff --git a/src/main/haskell/Language/Java/Streaming.hs b/src/main/haskell/Language/Java/Streaming.hs
new file mode 100644
--- /dev/null
+++ b/src/main/haskell/Language/Java/Streaming.hs
@@ -0,0 +1,267 @@
+-- | Expose Java iterators as streams from the
+-- <http://hackage.haskell.org/package/streaming streaming> package.
+
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE RecursiveDo #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# OPTIONS_GHC -fno-warn-unused-top-binds #-}
+{-# OPTIONS_GHC -fplugin=Language.Java.Inline.Plugin #-}
+
+module Language.Java.Streaming
+  ( reifyStreamWithBatching
+  , reflectStreamWithBatching
+  ) where
+
+import Control.Distributed.Closure.TH
+import Control.Monad.IO.Class (liftIO)
+import qualified Data.Coerce as Coerce
+import Data.IORef (newIORef, readIORef, writeIORef)
+import Data.Int (Int32, Int64)
+import Data.Proxy
+import qualified Data.Vector as V
+import Data.Singletons (SomeSing(..))
+import Foreign.Ptr (FunPtr, Ptr, freeHaskellFunPtr, intPtrToPtr, ptrToIntPtr)
+import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr)
+import qualified Foreign.JNI as JNI
+import qualified Foreign.JNI.Types as JNI
+import Foreign.JNI.Types (jnull)
+import GHC.Stable
+  ( castPtrToStablePtr
+  , castStablePtrToPtr
+  , deRefStablePtr
+  , freeStablePtr
+  , newStablePtr
+  )
+import Language.Java
+import Language.Java.Batching
+import Language.Java.Inline
+import Streaming (Bifunctor(first), Stream, Of)
+import qualified Streaming as Streaming
+import qualified Streaming.Prelude as Streaming
+
+imports "io.tweag.jvm.batching.*"
+imports "java.util.Iterator"
+
+type JNIFun a = JNIEnv -> Ptr JObject -> IO a
+
+foreign import ccall "wrapper" wrapObjectFun
+  :: JNIFun (Ptr (J ty)) -> IO (FunPtr (JNIFun (Ptr (J ty))))
+
+-- Export only to get a FunPtr.
+foreign export ccall "jvm_streaming_freeIterator" freeIterator
+  :: JNIEnv -> Ptr JObject -> Int64 -> IO ()
+foreign import ccall "&jvm_streaming_freeIterator" freeIteratorPtr
+  :: FunPtr (JNIEnv -> Ptr JObject -> Int64 -> IO ())
+
+data FunPtrTable = forall ty. FunPtrTable
+  { nextPtr :: FunPtr (JNIFun (Ptr (J ty)))
+  }
+
+freeIterator :: JNIEnv -> Ptr JObject -> Int64 -> IO ()
+freeIterator _ _ ptr = do
+    let sptr = castPtrToStablePtr $ intPtrToPtr $ fromIntegral ptr
+    FunPtrTable{..} <- deRefStablePtr sptr
+    freeHaskellFunPtr nextPtr
+    freeStablePtr sptr
+
+-- | Reflects a stream with no batching.
+newIterator
+  :: forall ty. Stream (Of (J ty)) IO ()
+  -> IO (J ('Iface "java.util.Iterator" <> '[ty]))
+newIterator stream0 = mdo
+    ref <- newIORef stream0
+    nextPtr <- wrapObjectFun $ \_ jthis ->
+      -- Conversion is safe, because result is always a reflected object.
+      unsafeForeignPtrToPtr <$> Coerce.coerce <$> popStream jthis
+    -- Keep FunPtr's in a table that can be referenced from the Java side, so
+    -- that they can be freed.
+    tblPtr :: Int64 <- fromIntegral . ptrToIntPtr . castStablePtrToPtr <$> newStablePtr FunPtrTable{..}
+    iterator <-
+      [java| new Iterator() {
+          /// A field that the Haskell side sets to true when it reaches the end.
+          private boolean end = false;
+          /// Lookahead element - it always points to a valid element unless
+          /// end is true. There is no constructor, so in order to initialize
+          // it, next() must be invoked once.
+          private Object lookahead;
+          @Override
+          public boolean hasNext() { return !end; }
+
+          @Override
+          public Object next() {
+            if (hasNext()) {
+              final Object temp = lookahead;
+              lookahead = hsNext();
+              return temp;
+            } else
+              throw new java.util.NoSuchElementException();
+          }
+
+          @Override
+          public void remove() { throw new UnsupportedOperationException(); }
+
+          private native void hsFinalize(long tblPtr);
+
+          private native Object hsNext();
+
+          @Override
+          public void finalize() { hsFinalize($tblPtr); }
+        } |]
+    klass <- JNI.getObjectClass iterator >>= JNI.newGlobalRef
+    fieldEndId <- JNI.getFieldID klass "end"
+                    (JNI.signature (sing :: Sing ('Prim "boolean")))
+    let popStream :: Ptr JObject -> IO (J ty)
+        popStream ptrThis = do
+          stream <- readIORef ref
+          Streaming.uncons stream >>= \case
+            Nothing -> do
+              jthis <- JNI.objectFromPtr ptrThis
+              -- When the stream ends, set the end field to True
+              -- so the Iterator knows not to call hsNext again.
+              JNI.setBooleanField jthis fieldEndId 1
+              return jnull
+            Just (x, stream') -> do
+              writeIORef ref stream'
+              return x
+    JNI.registerNatives klass
+      [ JNI.JNINativeMethod
+          "hsNext"
+          (methodSignature [] (sing :: Sing ('Class "java.lang.Object")))
+          nextPtr
+      , JNI.JNINativeMethod
+          "hsFinalize"
+          (methodSignature [SomeSing (sing :: Sing ('Prim "long"))] (sing :: Sing 'Void))
+          freeIteratorPtr
+      ]
+    -- Call next once to initialize the iterator.
+    () <- [java| { $iterator.next(); } |]
+    return $ generic iterator
+
+-- | Reifies streams from iterators in batches of the given size.
+reifyStreamWithBatching
+  :: forall a. BatchReify a
+  => Int32  -- ^ The batch size
+  -> J ('Iface "java.util.Iterator" <> '[Interp a])
+  -> IO (Stream (Of a) IO ())
+reifyStreamWithBatching batchSize jiterator0 = do
+    let jiterator1 = unsafeUngeneric jiterator0
+    jbatcher <- unsafeUngeneric <$> newBatchWriter (Proxy :: Proxy a)
+    jiterator <- [java| new Iterator() {
+        private final int batchSize = $batchSize;
+        private final Iterator it = $jiterator1;
+        private final BatchWriter batcher = $jbatcher;
+        public int count = 0;
+
+        @Override
+        public boolean hasNext() { return it.hasNext(); }
+
+        @Override
+        public Object next() {
+          int i = 0;
+          batcher.start(batchSize);
+          while (it.hasNext() && i < batchSize) {
+            batcher.set(i, it.next());
+            i++;
+          }
+          count = i;
+          return batcher.getBatch();
+        }
+
+        @Override
+        public void remove() {
+          throw new UnsupportedOperationException();
+        }
+      } |]
+      >>= JNI.newGlobalRef
+      :: IO (J ('Iface "java.util.Iterator"))
+    cls <- JNI.getObjectClass jiterator >>= JNI.newGlobalRef
+    fieldId <- JNI.getFieldID cls "count"
+                 (JNI.signature (sing :: Sing ('Prim "int")))
+
+    let go :: Int        -- next element to return from the batch
+           -> V.Vector a -- current batch of elements
+           -> Stream (Of a) IO ()
+        go i v =
+          if V.length v == i then do
+            hasNext <- liftIO [java| $jiterator.hasNext() |]
+            if hasNext then do
+              v' <- liftIO $
+                [java| $jiterator.next() |] `withLocalRef` \jbatch ->
+                  JNI.getIntField jiterator fieldId
+                  >>= reifyBatch (unsafeCast (jbatch :: JObject) :: J (Batch a))
+              go 0 v'
+            else
+              liftIO $ do
+                JNI.deleteGlobalRef jiterator
+                JNI.deleteGlobalRef cls
+          else do
+            Streaming.yield $ v V.! i
+            go (i + 1) v
+
+    return $ go 0 V.empty
+
+-- | Reflects streams to iterators in batches of the given size.
+reflectStreamWithBatching
+  :: forall a. BatchReflect a
+  => Int  -- ^ The batch size
+  -> Stream (Of a) IO ()
+  -> IO (J ('Iface "java.util.Iterator" <> '[Interp a]))
+reflectStreamWithBatching batchSize s0 = do
+    jiterator <- unsafeUngeneric <$>
+      (reflectStream $ Streaming.mapsM
+                        (\s -> first V.fromList <$> Streaming.toList s)
+                     $ Streaming.chunksOf batchSize s0
+      )
+    jbatchReader <- unsafeUngeneric <$> newBatchReader (Proxy :: Proxy a)
+    generic <$> [java| new Iterator() {
+        private final Iterator it = $jiterator;
+        private final BatchReader batchReader = $jbatchReader;
+        private int count = 0;
+
+        @Override
+        public boolean hasNext() {
+          return count < batchReader.getSize() || it.hasNext();
+        }
+        @Override
+        public Object next() {
+          if (count == batchReader.getSize()) {
+            batchReader.setBatch(it.next());
+            count = 0;
+          }
+          Object o = batchReader.get(count);
+          count++;
+          return o;
+        }
+        @Override
+        public void remove() {
+          throw new UnsupportedOperationException();
+        }
+      } |]
+  where
+    reflectStream :: Stream (Of (V.Vector a)) IO ()
+                  -> IO (J ('Iface "java.util.Iterator" <> '[Batch a]))
+    reflectStream = newIterator . Streaming.mapM reflectBatch
+
+withStatic [d|
+  instance Interpretation (Stream (Of a) m r) where
+    type Interp (Stream (Of a) m r) = 'Iface "java.util.Iterator"
+
+  instance BatchReify a => Reify (Stream (Of a) IO ()) where
+    reify = reifyStreamWithBatching 1024 . generic
+
+  instance BatchReflect a => Reflect (Stream (Of a) IO ()) where
+    reflect = fmap unsafeUngeneric . reflectStreamWithBatching 1024
+  |]
diff --git a/src/test/haskell/Language/Java/StreamingSpec.hs b/src/test/haskell/Language/Java/StreamingSpec.hs
new file mode 100644
--- /dev/null
+++ b/src/test/haskell/Language/Java/StreamingSpec.hs
@@ -0,0 +1,44 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# OPTIONS_GHC -fplugin=Language.Java.Inline.Plugin #-}
+
+module Language.Java.StreamingSpec where
+
+import Data.Int
+import Data.IORef
+import Language.Java
+import Language.Java.Inline
+import Language.Java.Streaming ()
+import Test.Hspec
+import Streaming (Stream, Of)
+import qualified Streaming.Prelude as Streaming
+
+spec :: Spec
+spec = do
+    describe "iteration" $ do
+      it "succeeds on empty lists" $ do
+        vals <- reflect [1..0 :: Int32]
+        iterator <- [java| java.util.Arrays.asList($vals).iterator() |]
+        stream <- reify iterator
+        Streaming.toList_ stream `shouldReturn` [1..0 :: Int32]
+      it "succeeds on singleton lists" $ do
+        vals <- reflect [1 :: Int32]
+        iterator <- [java| java.util.Arrays.asList($vals).iterator() |]
+        stream <- reify iterator
+        Streaming.toList_ stream `shouldReturn` [1 :: Int32]
+      it "succeeds on non-trivial lists" $ do
+        vals <- reflect [1..10000 :: Int32]
+        iterator <- [java| java.util.Arrays.asList($vals).iterator() |]
+        stream <- reify iterator
+        Streaming.toList_ stream `shouldReturn` [1..10000 :: Int32]
+    describe "streams" $ do
+      it "have the property that reify . reflect == id" $ do
+        iterator <- reflect (Streaming.each [1..10000] :: Stream (Of Int32) IO ())
+        stream <- reify iterator
+        Streaming.toList_ stream `shouldReturn` [1..10000 :: Int32]
+      it "don't redo effects" $ do
+        ref <- newIORef (1 :: Int32)
+        iterator <- reflect $
+          Streaming.replicateM 10 $ atomicModifyIORef ref (\i -> (i + 1, i))
+        stream <- reify iterator
+        Streaming.toList_ stream `shouldReturn` [1..10 :: Int32]
diff --git a/src/test/haskell/Main.hs b/src/test/haskell/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/test/haskell/Main.hs
@@ -0,0 +1,11 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import Language.Java (withJVM)
+import qualified Spec
+import Test.Hspec
+
+main :: IO ()
+main =
+    withJVM ["-Djava.class.path=../jvm-batching/build/libs/jvm-batching.jar"] $
+      hspec Spec.spec
diff --git a/src/test/haskell/Spec.hs b/src/test/haskell/Spec.hs
new file mode 100644
--- /dev/null
+++ b/src/test/haskell/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Spec #-}
diff --git a/tests/Language/Java/StreamingSpec.hs b/tests/Language/Java/StreamingSpec.hs
deleted file mode 100644
--- a/tests/Language/Java/StreamingSpec.hs
+++ /dev/null
@@ -1,44 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE QuasiQuotes #-}
-{-# OPTIONS_GHC -fplugin=Language.Java.Inline.Plugin #-}
-
-module Language.Java.StreamingSpec where
-
-import Data.Int
-import Data.IORef
-import Language.Java
-import Language.Java.Inline
-import Language.Java.Streaming ()
-import Test.Hspec
-import Streaming (Stream, Of)
-import qualified Streaming.Prelude as Streaming
-
-spec :: Spec
-spec = do
-    describe "iteration" $ do
-      it "succeeds on empty lists" $ do
-        vals <- reflect [1..0 :: Int32]
-        iterator <- [java| java.util.Arrays.asList($vals).iterator() |]
-        stream <- reify iterator
-        Streaming.toList_ stream `shouldReturn` [1..0 :: Int32]
-      it "succeeds on singleton lists" $ do
-        vals <- reflect [1 :: Int32]
-        iterator <- [java| java.util.Arrays.asList($vals).iterator() |]
-        stream <- reify iterator
-        Streaming.toList_ stream `shouldReturn` [1 :: Int32]
-      it "succeeds on non-trivial lists" $ do
-        vals <- reflect [1..10000 :: Int32]
-        iterator <- [java| java.util.Arrays.asList($vals).iterator() |]
-        stream <- reify iterator
-        Streaming.toList_ stream `shouldReturn` [1..10000 :: Int32]
-    describe "streams" $ do
-      it "have the property that reify . reflect == id" $ do
-        iterator <- reflect (Streaming.each [1..10000] :: Stream (Of Int32) IO ())
-        stream <- reify iterator
-        Streaming.toList_ stream `shouldReturn` [1..10000 :: Int32]
-      it "don't redo effects" $ do
-        ref <- newIORef (1 :: Int32)
-        iterator <- reflect $
-          Streaming.replicateM 10 $ atomicModifyIORef ref (\i -> (i + 1, i))
-        stream <- reify iterator
-        Streaming.toList_ stream `shouldReturn` [1..10 :: Int32]
diff --git a/tests/Main.hs b/tests/Main.hs
deleted file mode 100644
--- a/tests/Main.hs
+++ /dev/null
@@ -1,8 +0,0 @@
-module Main where
-
-import Language.Java (withJVM)
-import qualified Spec
-import Test.Hspec
-
-main :: IO ()
-main = withJVM [] $ hspec Spec.spec
diff --git a/tests/Spec.hs b/tests/Spec.hs
deleted file mode 100644
--- a/tests/Spec.hs
+++ /dev/null
@@ -1,1 +0,0 @@
-{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Spec #-}
