Spring 4 + Embedded Tomcat 7 - spring

I try to build a web app using Spring Web MVC 4.3.2 and embedded Tomcat 7.0.64.
I did not manage to write the correct main method to start embedded Tomcat. It works for Spring Controller sending #ResponseBody content (JSON) but failed for JSP views.
public static void main(String[] args) throws Exception {
String appBase = ".";// What to put here ?
Tomcat tomcat = new Tomcat();
String contextPath = "";
String port = System.getProperty("server.port");
tomcat.setPort(port == null ? 8080 : Integer.valueOf(port));
tomcat.getHost().setAppBase(appBase);
Context context = tomcat.addWebapp(contextPath, appBase);
// So that it works when in it's launched from IntelliJ or Eclipse
// Also need that a folder named "META-INF" exists in build/classes/main
// https://bz.apache.org/bugzilla/show_bug.cgi?id=52853#c19
((StandardJarScanner) context.getJarScanner()).setScanAllDirectories(true);
tomcat.start();
tomcat.getServer().await();
}
For JSP view it says : The requested resource is not available (WEB-INF/views/home.jsp) HTTP 404
If I set the appBase variable to the absolute path where the JSPs are, it works. But, of course, it is not a solution as it would not work on another machine. I need a relative path.
If I set appBase varibale to "src/main/webapp", then Tomcat fails to start with the following error : java.lang.IllegalArgumentException: Document base C:\blabla\spring-jsp-embedded-tomcat\tomcat.8080\src\main\webapp\src\main\webapp does not exist or is not a readable directory.
Morevover, the jar that is built with Gradle fat jar technique does not contain the WEB-INF dir.
How can I do to make a simple Spring MVC app working with an embedded Tomcat and JSPs (to be launched with java -cp path/to/my/jar com.app.Launcher) ?
build.gradle :
apply plugin: 'java'
sourceCompatibility = 1.7
version = '1.0'
jar {
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
}
repositories {
maven { url "http://repo1.maven.org/maven2" }
}
dependencies {
compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.6.2'
compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.6.2'
compile group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.6.2'
compile 'org.springframework:spring-webmvc:4.3.2.RELEASE'
compile 'com.fasterxml.jackson.core:jackson-databind:2.7.0'
compile 'javax.servlet:javax.servlet-api:3.0.1'
compile 'javax.servlet.jsp:jsp-api:2.2'
compile 'javax.servlet:jstl:1.2'
// Embedded Tomcat
// 2 mandatory libs
compile 'org.apache.tomcat.embed:tomcat-embed-core:7.0.64'
compile 'org.apache.tomcat.embed:tomcat-embed-logging-juli:7.0.64'
// To enable JSPs
compile 'org.apache.tomcat.embed:tomcat-embed-jasper:7.0.64'
testCompile group: 'junit', name: 'junit', version: '4.+'
}
Tomcat launcher :
public class Launcher {
public static void main(String[] args) throws Exception {
String contextPath = "";
// String appBase = "C:/absolute/path/to/webapp/dir"; // It works but of course I need a relative path
// String appBase = "."; // Works only for Controller sending back ResponseBody (JSON) but fail to find jsp files
String appBase = "src/main/webapp"; // Tomcat does not start properly
Tomcat tomcat = new Tomcat();
String port = System.getProperty("server.port");
tomcat.setPort(port == null ? 8080 : Integer.valueOf(port));
tomcat.getHost().setAppBase(appBase);
Context context = tomcat.addWebapp(contextPath, appBase);
// So that it works when in it's launched from IntelliJ or Eclipse
// Also need that a folder named "META-INF" exists in build/classes/main
// https://bz.apache.org/bugzilla/show_bug.cgi?id=52853#c19
((StandardJarScanner) context.getJarScanner()).setScanAllDirectories(true);
tomcat.start();
tomcat.getServer().await();
}
}
Spring web app initializer :
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
#Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
#Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
#Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { WebConfig.class };
}
}
WebConfig :
#Configuration
#EnableWebMvc
#ComponentScan("com.app")
public class WebConfig extends WebMvcConfigurerAdapter {
#Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
resolver.setExposeContextBeansAsAttributes(true);
return resolver;
}
#Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
Folder structure :

Apparently, embedded Tomcat expects static resources to be in a META-INF/resources directory. I followed this : tutorial and I checked how the final jar was structured.
So I modified the Gradle build script to put the JSPs there.
sourceSets {
main {
resources.srcDirs = ["src/main/webapp"]
output.resourcesDir = "$buildDir/classes/main/META-INF/resources"
}
}
And now it works. However, I have the feeling that it's a makeshift job. If someone has a more satisfying and educational answer, I would be pleased to get it.

Related

NoSuchMethodException QueryDSL with Spring Boot & Spring Data Mongo

I am trying to implement Query DSL on my Spring Boot 2.0.4.RELEASE app that uses Spring Data Mongo 2.0.4.RELEASE & Gradle 4.10.
I am using Spring Tool Suite for running it locally.
Did the following steps which I found from multiple sources including Spring data documentation:
created gradle/querydsl.gradle which has below content to generate Q classes
apply plugin: "com.ewerk.gradle.plugins.querydsl"
sourceSets {
main {
java {
srcDir "$buildDir/generated/source/apt/main"
}
}
}
querydsl {
springDataMongo = true
querydslSourcesDir = "$buildDir/generated/source/apt/main"
}
dependencies {
compile "com.querydsl:querydsl-mongodb:4.1.4"
compileOnly "com.querydsl:querydsl-apt:4.1.4"
}
sourceSets.main.java.srcDirs = ['src/main/java']
Calling above gradle file from main build.gradle as shown below
buildscript {
ext { springBootVersion = "2.0.4.RELEASE" }
repositories { mavenCentral() }
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}"
classpath "gradle.plugin.com.ewerk.gradle.plugins:querydsl-plugin:1.0.9"
}
}
plugins {
id "java"
id "eclipse"
id "org.springframework.boot" version "2.0.4.RELEASE"
id "io.spring.dependency-management" version "1.0.6.RELEASE"
}
sourceCompatibility = 1.8
repositories { mavenCentral() }
dependencies {
...
compile("org.springframework.boot:spring-boot-starter-web:${springBootVersion}")
compile("org.springframework.boot:spring-boot-starter-data-mongodb:${springBootVersion}")
...
}
apply from: 'gradle/querydsl.gradle'
/* Added this because Eclipse was not able to find generated classes */
sourceSets.main.java.srcDirs = ['build/generated/source/apt/main','src/main/java']
compileJava.dependsOn processResources
processResources.dependsOn cleanResources
After this updated the Repository annotated interface as below. Note: I also use Fragment Repository FragmentOrderRepository for some custom queries.
public interface OrderRepository<D extends OrderDAO>
extends EntityRepository<D>, PagingAndSortingRepository<D, String>, FragmentOrderRepository<D>, QuerydslPredicateExecutor<D> {}
Then in controller created a GET mapping as shown here
#RestController
public class OrderController {
#GetMapping(value="/orders/dsl", produces = { "application/json" })
public ResponseEntity<List> getOrdersDSL(#QuerydslPredicate(root = OrderDAO.class) Predicate predicate, Pageable pageable, #RequestParam final MultiValueMap<String, String> parameters) {
return (ResponseEntity<List>) orderService.getTools().getRepository().findAll(predicate, pageable);
}
}
Then in my runner class I added EnableSpringDataWebSupport annotation
#SpringBootApplication
#EnableSpringDataWebSupport
public class SampleApp {
public static void main(String[] args) {
SpringApplication.run(SampleApp.class, args);
}
}
With this my app starts up without any errors but when I try hitting the path http://localhost:5057/orders/dsl?email=test#test.com
I get a NoSuchMethodException with message No primary or default constructor found for interface com.querydsl.core.types.Predicate.
Can anyone please help with some pointers to solve this issue?
It seems that parameters are not getting resolved to a type.
---- UPDATE 09/19/19 ----
While debugging I found that a class HandlerMethodArgumentResolverComposite which finds ArgumentResolver for given MethodParameter from a List of argumentResolvers(of type HandlerMethodArgumentResolver). This list does not contain QuerydslPredicateArgumentResolver. Hence it is not able to resolve the arguments.
This means QuerydslWebConfiguration which adds above resolver for Predicate type is not getting called, which in turn indicates that some AutoConfiguration is not happening.
Probably I am missing some annotation here.
Found the mistake I was doing, was missing EnableWebMvc annotation on my Configuration annotated class.
Details are in this documentation.

Circular view path [error] while json output expected in springboot

newbie to Springboot with gradle, I am creating a restful service which queries the db2 database and returns the result in json.
The desired output
{
resource: {
results: [
{
currencyCode: "JPY",
conversionRateToUSD: "0.010286580",
conversionRateFromUSD: "97.214040040",
startDate: "2011-01-01",
endDate: "2011-01-29"
}
]
}
}
The api i am trying to build is http://localhost:8080/apis/exchange-rates/referenceDate=2015-01-01&currencyCode=JPY
I have created the below controller class
#RestController
#Slf4j
#RequestMapping("/apis")
public class IndividualExchangeRateController {
#Autowired
private IndividualExchangeRateService individualExchangeRateService;
public IndividualExchangeRateController(IndividualExchangeRateService individualExchangeRateService) {
this.individualExchangeRateService = individualExchangeRateService;
}
#RequestMapping(value = "/exchange-rates", method = RequestMethod.GET)
#ResponseStatus(HttpStatus.OK)
public #ResponseBody IndividualResource getIndividual(#RequestParam("referenceDate") #DateTimeFormat(pattern = "YYYY-MM-DD")Date referenceDate,
#RequestParam(value = "currencyCode", required = false) String currencyCode){
try {
System.out.println("Inside Controller");
return individualExchangeRateService.getIndividualExchangeRate(referenceDate, currencyCode);
}
catch (HttpClientErrorException e){
throw new InvalidRequestException(e.getMessage());
}
}
}
I am getting the below error when i call the api
javax.servlet.ServletException: Circular view path [error]: would dispatch back to the current handler URL [/error] again. Check your ViewResolver setup! (Hint: This may be the result of an unspecified view, due to default view name generation.)
Can anybody help out on this ?
As the output is json i do not have thymeleaf dependencies on my application
Below is the gradle build file
plugins {
id 'org.springframework.boot' version '2.1.7.RELEASE'
id 'java'
}
apply plugin: 'io.spring.dependency-management'
group = 'com.abc.service'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
maven { url "http://artifactory.abcinc.dev/artifactory/maven-repos" }
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
compileOnly 'org.projectlombok:lombok:1.18.8'
annotationProcessor 'org.projectlombok:lombok:1.18.8'
runtime 'com.ibm.db2.jcc:db2jcc4:4.19.49'
compile group: 'org.springframework', name: 'spring-jdbc', version: '5.1.9.RELEASE'
compile group: 'com.zaxxer', name: 'HikariCP', version: '3.3.1'
compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.9.9'
}
public class IndividualExchangeRate {
private String currencyCode;
private double conversionRateFromUSD;
private double conversionRateToUSD;
}
public class IndividualResource {
private List<IndividualExchangeRate> individualExchangeRates;
}
All the classes are annotated with lombok.
Spring Boot uses a default Whitelabel error page in case server error.So there might be some code snippets which is breaking your appliaction flow.
Add server.error.whitelabel.enabled=false in your application.properties file .
OR add the following code snippets :
#Controller
public class AppErrorController implements ErrorController{
private static final String PATH = "/error";
#RequestMapping(value = PATH)
public String error() {
return "Error handling";
}
#Override
public String getErrorPath() {
return PATH;
}
}

Gradle + Embedded Jetty

I have modified an existing Jetty project and after I builded, I got 404. Maybe I need to modify other files which I do not know.
I am using gradle to build. Here is the build.gradle :
apply plugin: 'java'
apply plugin: 'jetty'
apply plugin: 'eclipse'
repositories {
mavenCentral()
}
dependencies {
compile 'org.springframework:spring-webmvc:4.1.6.RELEASE'
compile 'org.hibernate:hibernate-core:4.3.6.Final'
testImplementation 'junit:junit:4.12'
testCompile 'junit:junit:4.11'
testCompile 'org.hamcrest:hamcrest-all:1.3'
compile 'org.glassfish.jersey.containers:jersey-container-servlet:2.14'
}
test {
exclude '**/*IntegrationTest*'
}
task integrationTest(type: Test) {
include '**/*IntegrationTest*'
doFirst {
jettyRun.contextPath = '/';
jettyRun.httpPort = 8080 // Port for test
jettyRun.daemon = true
jettyRun.execute()
}
doLast {
jettyStop.stopPort = 8091 // Port for stop signal
jettyStop.stopKey = 'stopKey'
jettyStop.execute()
}
}
// Embeded Jetty for testing
jettyRun{
contextPath = "spring4"
httpPort = 8080
}
jettyRunWar{
contextPath = "spring4"
httpPort = 8080
}
//For Eclipse IDE only
eclipse {
wtp {
component {
//define context path, default to project folder name
contextPath = 'spring4'
}
}
}
Here the other class:
package com.hello.webapp;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import com.hello.service.SignUpService;
#Path("/signUp")
public class SignUpWebapp {
private static SignUpService signUpService = new SignUpService();
#GET()
public String hello() {
return signUpService.sayHello();
}
}
here the simple service:
package com.hello.service;
public class SignUpService {
public String sayHello() {
return "signUp";
}
}
this is another the integration test class
package com.hello.webapp;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import org.junit.Test;
public class SignUpIntegrationTest {
private static String SIGNUP = "http://localhost:8080/signUp";
#Test
public void testHello() throws Exception {
Client client = ClientBuilder.newClient();
WebTarget webTarget = client.target(SIGNUP);
String response = webTarget.request().get(String.class);
assertThat(response, is("signUp"));
}
}
So, when I run gradle integrationTest I get an error saying that Jetty is already running. And when I try to visit localhost/signUp I get a 404.
One solution is to use gretty instead:
apply plugin: 'org.akhikhl.gretty'
....
classpath 'org.akhikhl.gretty:gretty:1.4.0'
...
gretty {
port = 8083
contextPath = '/'
servletContainer = 'jetty9'
jvmArgs = [
'-Xms700m',
'-Xmx2048m'
]
loggingLevel='INFO'
debugPort = 5005 // default
debugSuspend = true // default
httpsEnabled = true
httpsPort = 8443
sslKeyStorePath = 'some.jks'
sslKeyStorePassword = 'somepwd'
}
and then gradle appStart
Let me know if that helps.

Error 406 on Spring 3+Google App Engine on Android Studio

I am using Google App Engine to develop a backend module provided by Android Studio. I am also using Spring 3 to map URL to class.Although,I have successfully used the combo on Eclipse.But on Android Studio getting a peculiar error(May be because of gradle error).Consider the following files-
build.gradle
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.google.appengine:gradle-appengine-plugin:1.9.18'
}
}
repositories {
jcenter();
}
apply plugin: 'java'
apply plugin: 'war'
apply plugin: 'appengine'
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
dependencies {
appengineSdk 'com.google.appengine:appengine-java-sdk:1.9.18'
compile 'javax.servlet:servlet-api:2.5'
compile 'org.springframework:spring-webmvc:4.0.0.RELEASE'
compile 'com.google.code.gson:gson:2.5'
}
appengine {
downloadSdk = true
appcfg {
oauth2 = true
}
}
MyServlet.java . When I am accessing conventional MyServlet the Gson get successfully loaded and returns the required JSON.
public class MyServlet extends HttpServlet {
#Override
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
resp.setContentType("application/json");
Gson gson = new Gson();
Shop sh = new Shop();
sh.setName("Gufran Kurshid");
sh.setAddress("Pitampura new Delhi");
sh.setId(5235);
resp.getWriter().println(gson.toJson(sh));
}
}
MyController.java - A normal controller URL /hello is also easily accessible.But /getDummyJSON which is made to return JSON is giving Error 406.
#Controller
#RequestMapping("/greet")
public class MyController {
#RequestMapping("/hello")
public ModelAndView helloDost() {
ModelAndView modelAndView = new ModelAndView("rat");
modelAndView.addObject("message", "Hello Dost");
return modelAndView;
}
#RequestMapping(value = "/getDummyJSON", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET)
public
#ResponseBody
Map getDummyJSON() {
Map map = new HashMap<String, Object>();
map.put("status", true);
map.put("message", "Hello Friends");
return map;
}
}
My study says the Spring is not able map and load Gson classes.Is there any thing wrong with Android Studio or my Gradle file ?

How to specify context path for jersey test with external provider

I want my jersey tests to run on one instance of tomcat which has the rest services running at
http://myhost:port/contexpath/service1/
http://myhost:port/contexpath/service2/
..so on
I have both in memory and external container dependencies
[ group: 'org.glassfish.jersey.test-framework.providers', name: 'jersey-test-framework-provider-inmemory', version: '2.17'],
[group: 'org.glassfish.jersey.test-framework.providers', name: 'jersey-test-framework-provider-external' , version: '2.17'],
Then in the test i over ride the below method to decide which container to choose
#Override
public TestContainerFactory getTestContainerFactory() {
System.setProperty("jersey.test.host", "localhost");
System.setProperty("jersey.config.test.container.port", "8000");
//how to set the context path ??
return new ExternalTestContainerFactory();
}
The in memory test works because the services are deployed by the framework at path which it knows(it does not have a context path anyway)
When i run on external container it tries to connect to http://myhost:port/service1/ instead of http://myhost:port/contexpath/service1/ thus getting 404 not found
To run on an external container how do i specify the context path ?
The documentation specifies only host and port property.Is there any property for context path ?
I am using Jersey 2.17
Finally I figured out a solution
#Override
public TestContainerFactory getTestContainerFactory() {
return new ExternalTestContainerFactory(){
#Override
public TestContainer create(URI baseUri, DeploymentContext context)
throws IllegalArgumentException {
try {
baseUri = new URI("http://localhost:8000/contextpath");
} catch (URISyntaxException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return super.create(baseUri, context);
}
};
}
If you have your external servlet:
Import the jersey-test-framework-core apis to implement your own TestContainerFactory
testCompile 'org.glassfish.jersey.test-framework:jersey-test-framework-core:2.22.2'
.
Let JerseyTest know you will have your own provider through SystemProperties
systemProperty 'jersey.config.test.container.factory', 'my.package.MyTestContainerFactory'
.
Create your own provider (better and more custom configurable than their jersey-test-framework-provider-external)
import org.glassfish.jersey.test.spi.TestContainer;
import org.glassfish.jersey.test.spi.TestContainerFactory;
public class MyTestContainerFactory implements TestContainerFactory {
#Override
public TestContainer create(URI baseUri, DeploymentContext deploymentContext) {
return new TestContainer(){
#Override
public ClientConfig getClientConfig() {
return null;
}
#Override
public URI getBaseUri() {
return URI.create("http://localhost:8080/myapp/api");
}
#Override
public void start() {
// Do nothing
}
#Override
public void stop() {
// Do nothing
}
};
}
}

Resources